-
Notifications
You must be signed in to change notification settings - Fork 484
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: update issue template and Gotchas guide
The Gotchas guide hasn't been updated in quite some time. This refresh adds a more structured layout with explicit examples and solutions for the most common issues. This also updates the issue template to add a note about what constitutes an appropriate issue, and a link to the gotchas guide.
- Loading branch information
1 parent
6fc561b
commit 394f30f
Showing
2 changed files
with
242 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,231 @@ | ||
# Universal "Gotchas" | ||
|
||
When building Universal components in Angular there are a few things to keep in mind. | ||
|
||
- **`window`**, **`document`**, **`navigator`**, and other browser types - _do not exist on the server_ - so using them, or any library that uses them (jQuery for example) will not work. You do have some options, if you truly need some of this functionality: | ||
- If you need to use them, consider limiting them to only your client and wrapping them situationally. You can use the Object injected using the PLATFORM_ID token to check whether the current platform is browser or server. | ||
|
||
```typescript | ||
import { PLATFORM_ID } from '@angular/core'; | ||
import { isPlatformBrowser, isPlatformServer } from '@angular/common'; | ||
|
||
constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... } | ||
|
||
ngOnInit() { | ||
if (isPlatformBrowser(this.platformId)) { | ||
// Client only code. | ||
... | ||
} | ||
if (isPlatformServer(this.platformId)) { | ||
// Server only code. | ||
... | ||
} | ||
} | ||
``` | ||
|
||
- Try to *limit or* **avoid** using **`setTimeout`**. It will slow down the server-side rendering process. Make sure to remove them in the [`ngOnDestroy`](https://angular.io/docs/ts/latest/api/core/index/OnDestroy-class.html) method of your Components. | ||
- Also for RxJs timeouts, make sure to _cancel_ their stream on success, for they can slow down rendering as well. | ||
- **Don't manipulate the nativeElement directly**. Use the _Renderer2_. We do this to ensure that in any environment we're able to change our view. | ||
``` | ||
constructor(element: ElementRef, renderer: Renderer2) { | ||
renderer.setStyle(element.nativeElement, 'font-size', 'x-large'); | ||
# Important Considerations when Using Angular Universal | ||
|
||
## Introduction | ||
|
||
Although the goal of the Universal project is the ability to seamlessly render an Angular | ||
application on the server, there are some inconsistencies that you should consider. First, | ||
there is the obvious discrepancy between the server and browser environments. When rendering | ||
on the server, your application is in an ephemeral or "snapshot" state. The application is | ||
fully rendered once, with the resulting HTML returned, and the remaining application state | ||
destroyed until the next render. Next, the server environment inherently does not have the | ||
same capabilities as the browser (and has some that likewise the browser does not). For | ||
instance, the server does not have any concept of cookies. You can polyfill this and other | ||
functionality, but there is no perfect solution for this. In later sections, we'll walk | ||
through potential mitigations to reduce the error plane when rendering on the server. | ||
|
||
Please also note the goal of SSR: improved initial render time for your application. This | ||
means that anything that has the potential to reduce the speed of your application in this | ||
initial render should be avoided or sufficiently guarded against. Again, we'll review how | ||
to accomplish this in later sections. | ||
|
||
## "window is not defined" | ||
|
||
One of the most common issues when using Angular Universal is the lack of browser global | ||
variables in the server environment. This is because the Universal project uses | ||
[domino](https://github.com/fgnass/domino) as the server DOM rendering engine. As a result, | ||
there is certain functionality that won't be present or supported on the server. This | ||
includes the `window` and `document` global objects, cookies, certain HTML elements (like canvas), | ||
and several others. There is no exhaustive list, so please be cognizant of the fact that if you | ||
see an error like this, where a previously-accessible global is not defined, it's likely because | ||
that global is not available through domino. | ||
|
||
> Fun fact: Domino stands for "DOM in Node" | ||
### How to fix? | ||
|
||
#### Strategy 1: Injection | ||
|
||
Frequently, the needed global is available through the Angular platform via Dependency Injection (DI). | ||
For instance, the global `document` is available through the `DOCUMENT` token. Additionally, a _very_ | ||
primitive version of both `window` and `location` exist through the `DOCUMENT` object. For example: | ||
|
||
```ts | ||
// example.service.ts | ||
import {Injectable, Inject} from '@angular/core'; | ||
import {DOCUMENT} from '@angular/common'; | ||
|
||
@Injectable() | ||
export class ExampleService { | ||
constructor(@Inject(DOCUMENT) private _doc: Document) { | ||
} | ||
|
||
getWindow(): Window | null { | ||
return this._doc.defaultView; | ||
} | ||
|
||
getLocation(): Location { | ||
return this._doc.location; | ||
} | ||
|
||
createElement(tag: string): HTMLElement { | ||
return this._doc.createElement(tag); | ||
} | ||
} | ||
``` | ||
|
||
Please be judicious about using these references, and lower your expectations about their capabilities. `localStorage` | ||
is one frequently-requested API that won't work how you want it to out of the box. If you need to write your own library | ||
components, please consider using this method to provide similar functionality on the server (this is what Angular CDK | ||
and Material do). | ||
|
||
#### Strategy 2: Guards | ||
|
||
If you can't inject the proper global value you need from the Angular platform, you can "guard" against | ||
invocation of browser code, so long as you don't need to access that code on the server. For instance, | ||
often invocations of the global `window` element are to get window size, or some other visual aspect. | ||
However, on the server, there is no concept of "screen", and so this functionality is rarely needed. | ||
|
||
You may read online and elsewhere that the recommended approach is to use `isPlatformBrowser` or | ||
`isPlatformServer`. This guidance is **incorrect**. Instead, you should use Angular's Dependency Injection (DI) | ||
in order to remove the offending code and drop in a replacement at runtime. Here's an example: | ||
|
||
```ts | ||
// window-service.ts | ||
import {Injectable} from '@angular/core'; | ||
|
||
@Injectable() | ||
export class WindowService { | ||
getWidth(): number { | ||
return window.innerWidth; | ||
} | ||
} | ||
``` | ||
|
||
```ts | ||
// server-window.service.ts | ||
import {Injectable} from '@angular/core'; | ||
import {WindowService} from './window.service'; | ||
|
||
@Injectable() | ||
export class ServerWindowService extends WindowService { | ||
getWidth(): number { | ||
return 0; | ||
} | ||
} | ||
``` | ||
|
||
```ts | ||
// app-server.module.ts | ||
import {NgModule} from '@angular/core'; | ||
import {WindowService} from './window.service'; | ||
import {ServerWindowService} from './server-window.service'; | ||
|
||
@NgModule({ | ||
providers: [{ | ||
provide: WindowService, | ||
useClass: ServerWindowService, | ||
}] | ||
}) | ||
``` | ||
|
||
If you have a component provided by a third-party that is not Universal-compatible out of the box, | ||
you can create two separate modules for browser and server (the server module you should already have), | ||
in addition to your base app module. The base app module will contain all of your platform-agnostic code, | ||
the browser module will contain all of your browser-specific/server-incompatible code, and vice-versa for | ||
your server module. In order to avoid editing too much template code, you can create a no-op component | ||
to drop in for the library component. Here's an example: | ||
|
||
```ts | ||
// example.component.ts | ||
import {Component} from '@angular/core'; | ||
|
||
@Component({ | ||
selector: 'example-component', | ||
template: `<library-component></library-component>` // this is provided by a third-party lib | ||
// that causes issues rendering on Universal | ||
}) | ||
export class ExampleComponent { | ||
} | ||
``` | ||
|
||
```ts | ||
// app.module.ts | ||
import {NgModule} from '@angular/core'; | ||
import {ExampleComponent} from './example.component'; | ||
|
||
@NgModule({ | ||
declarations: [ExampleComponent], | ||
}) | ||
``` | ||
|
||
```ts | ||
// browser-app.module.ts | ||
import {NgModule} from '@angular/core'; | ||
import {LibraryModule} from 'some-lib'; | ||
import {AppModule} from './app.module'; | ||
|
||
@NgModule({ | ||
imports: [AppModule, LibraryModule], | ||
}) | ||
``` | ||
|
||
```ts | ||
// library-shim.component.ts | ||
import {Component} from '@angular/core'; | ||
|
||
@Component({ | ||
selector: 'library-component', | ||
template: '' | ||
}) | ||
export class LibraryShimComponent { | ||
} | ||
``` | ||
|
||
```ts | ||
// server.app.module.ts | ||
import {NgModule} from '@angular/core'; | ||
import {LibraryShimComponent} from './library-shim.component'; | ||
import {AppModule} from './app.module'; | ||
|
||
@NgModule({ | ||
imports: [AppModule], | ||
declarations: [LibraryShimComponent], | ||
}) | ||
export class ServerAppModule { | ||
} | ||
``` | ||
- The application runs XHR requests on the server & once again on the Client-side (when the application bootstraps) | ||
- Use a cache that's transferred from server to client (TODO: Point to the example) | ||
- Know the difference between attributes and properties in relation to the DOM. | ||
- Keep your directives stateless as much as possible. For stateful directives, you may need to provide an attribute that reflects the corresponding property with an initial string value such as url in img tag. For our native element the src attribute is reflected as the src property of the element type HTMLImageElement. | ||
|
||
#### Strategy 3: Shims | ||
|
||
If all else fails, and you simply must have access to some sort of browser functionality, you can patch | ||
the global scope of the server environment to include the globals you need. For instance: | ||
|
||
```ts | ||
// server.ts | ||
global['window'] = { | ||
// properties you need implemented here... | ||
}; | ||
``` | ||
|
||
This can be applied to any undefined element. Please be careful when you do this, as playing with the global | ||
scope is generally considered an anti-pattern. | ||
|
||
> Fun fact: a shim is a patch for functionality that will never be supported on a given platform. A | ||
> polyfill is a patch for functionality that is planned to be supported, or is supported on newer versions | ||
## Application is slow, or worse, won't render | ||
|
||
The Angular Universal rendering process is straightforward, but just as simply can be blocked or slowed down | ||
by well-meaning or innocent-looking code. First, some background on the rendering process. When a render | ||
request is made for platform-server (the Angular Universal platform), a single route navigation is executed. | ||
When that navigation completes, meaning that all Zone.js macrotasks are completed, the DOM in whatever state | ||
it's in at that time is returned to the user. | ||
|
||
> A Zone.js macrotask is just a JavaScript macrotask that executes in/is patched by Zone.js | ||
This means that if there is a process, like a microtask, that takes up ticks to complete, or a long-standing | ||
HTTP request, the rendering process will not complete, or will take longer. Macrotasks include calls to globals | ||
like `setTimeout` and `setInterval`, and `Observables`. Calling these without cancelling them, or letting them run | ||
longer than needed on the server could result in suboptimal rendering. | ||
|
||
> It may be worth brushing up on the JavaScript event loop and learning the difference between microtasks | ||
> and macrotasks, if you don't know it already. [Here's](https://javascript.info/event-loop) a good reference. | ||
## My X, Y, Z won't finish before render! | ||
|
||
Similarly to the above section on waiting for macrotasks to complete, the flip-side is that the platform will | ||
not wait for microtasks to complete before finishing the render. In Angular Universal, we have patched the | ||
Angular HTTP client to turn it into a macrotask, to ensure that any needed HTTP requests complete for a given | ||
render. However, this type of patch may not be appropriate for all microtasks, and so it is recommended you use | ||
your best judgment on how to proceed. You can look at the code reference for how Universal wraps a task to turn | ||
it into a macrotask, or you can simply opt to change the server behavior of the given tasks. |