Skip to content
This repository was archived by the owner on Nov 22, 2024. It is now read-only.

Commit 394f30f

Browse files
CaerusKarualan-agius4
authored andcommitted
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.
1 parent 6fc561b commit 394f30f

File tree

2 files changed

+242
-37
lines changed

2 files changed

+242
-37
lines changed

.github/ISSUE_TEMPLATE/Bug.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
<--
2+
**NOTE**: If you provide an incomplete template, the Universal team is liable to close your issue sight on scene.
3+
Please provide _as much detail as possible_ in as minimal a reproduction as possible. This means no third-party
4+
libraries, no application-specific code, etc. Essentially, we're looking for an `ng new` project with maybe one
5+
additional file/change. If this is not possible, please look deeper into your issue or post it to StackOverflow
6+
for support. This is a bug/feature tracker, meaning that something is actively _missing or broken_ in Angular Universal.
7+
Anything else does not belong on this forum.
8+
9+
For very common issues when using Angular Universal, please see [this doc](docs/gotchas.md). This includes
10+
issues for `window is not defined` and other hiccups when adopting Angular Universal for the first time.
11+
-->
12+
113
---
214
name: 🐞 Bug Report
315
about: Something is broken in Angular Universal
@@ -12,11 +24,10 @@ about: Something is broken in Angular Universal
1224
- [ ] common
1325
- [ ] express-engine
1426
- [ ] hapi-engine
15-
- [ ] module-map-ngfactory-loader
1627

1728
### Is this a regression?
1829

19-
<!-- Did this behavior use to work in the previous version? -->
30+
<!-- Did this behavior used to work in the previous version? -->
2031
<!-- ✍️--> Yes, the previous version in which this bug was not present was: ....
2132

2233
### Description

docs/gotchas.md

Lines changed: 229 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,231 @@
1-
# Universal "Gotchas"
2-
3-
When building Universal components in Angular there are a few things to keep in mind.
4-
5-
- **`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:
6-
- 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.
7-
8-
```typescript
9-
import { PLATFORM_ID } from '@angular/core';
10-
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
11-
12-
constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }
13-
14-
ngOnInit() {
15-
if (isPlatformBrowser(this.platformId)) {
16-
// Client only code.
17-
...
18-
}
19-
if (isPlatformServer(this.platformId)) {
20-
// Server only code.
21-
...
22-
}
23-
}
24-
```
25-
26-
- 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.
27-
- Also for RxJs timeouts, make sure to _cancel_ their stream on success, for they can slow down rendering as well.
28-
- **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.
29-
```
30-
constructor(element: ElementRef, renderer: Renderer2) {
31-
renderer.setStyle(element.nativeElement, 'font-size', 'x-large');
1+
# Important Considerations when Using Angular Universal
2+
3+
## Introduction
4+
5+
Although the goal of the Universal project is the ability to seamlessly render an Angular
6+
application on the server, there are some inconsistencies that you should consider. First,
7+
there is the obvious discrepancy between the server and browser environments. When rendering
8+
on the server, your application is in an ephemeral or "snapshot" state. The application is
9+
fully rendered once, with the resulting HTML returned, and the remaining application state
10+
destroyed until the next render. Next, the server environment inherently does not have the
11+
same capabilities as the browser (and has some that likewise the browser does not). For
12+
instance, the server does not have any concept of cookies. You can polyfill this and other
13+
functionality, but there is no perfect solution for this. In later sections, we'll walk
14+
through potential mitigations to reduce the error plane when rendering on the server.
15+
16+
Please also note the goal of SSR: improved initial render time for your application. This
17+
means that anything that has the potential to reduce the speed of your application in this
18+
initial render should be avoided or sufficiently guarded against. Again, we'll review how
19+
to accomplish this in later sections.
20+
21+
## "window is not defined"
22+
23+
One of the most common issues when using Angular Universal is the lack of browser global
24+
variables in the server environment. This is because the Universal project uses
25+
[domino](https://github.com/fgnass/domino) as the server DOM rendering engine. As a result,
26+
there is certain functionality that won't be present or supported on the server. This
27+
includes the `window` and `document` global objects, cookies, certain HTML elements (like canvas),
28+
and several others. There is no exhaustive list, so please be cognizant of the fact that if you
29+
see an error like this, where a previously-accessible global is not defined, it's likely because
30+
that global is not available through domino.
31+
32+
> Fun fact: Domino stands for "DOM in Node"
33+
34+
### How to fix?
35+
36+
#### Strategy 1: Injection
37+
38+
Frequently, the needed global is available through the Angular platform via Dependency Injection (DI).
39+
For instance, the global `document` is available through the `DOCUMENT` token. Additionally, a _very_
40+
primitive version of both `window` and `location` exist through the `DOCUMENT` object. For example:
41+
42+
```ts
43+
// example.service.ts
44+
import {Injectable, Inject} from '@angular/core';
45+
import {DOCUMENT} from '@angular/common';
46+
47+
@Injectable()
48+
export class ExampleService {
49+
constructor(@Inject(DOCUMENT) private _doc: Document) {
50+
}
51+
52+
getWindow(): Window | null {
53+
return this._doc.defaultView;
54+
}
55+
56+
getLocation(): Location {
57+
return this._doc.location;
58+
}
59+
60+
createElement(tag: string): HTMLElement {
61+
return this._doc.createElement(tag);
62+
}
63+
}
64+
```
65+
66+
Please be judicious about using these references, and lower your expectations about their capabilities. `localStorage`
67+
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
68+
components, please consider using this method to provide similar functionality on the server (this is what Angular CDK
69+
and Material do).
70+
71+
#### Strategy 2: Guards
72+
73+
If you can't inject the proper global value you need from the Angular platform, you can "guard" against
74+
invocation of browser code, so long as you don't need to access that code on the server. For instance,
75+
often invocations of the global `window` element are to get window size, or some other visual aspect.
76+
However, on the server, there is no concept of "screen", and so this functionality is rarely needed.
77+
78+
You may read online and elsewhere that the recommended approach is to use `isPlatformBrowser` or
79+
`isPlatformServer`. This guidance is **incorrect**. Instead, you should use Angular's Dependency Injection (DI)
80+
in order to remove the offending code and drop in a replacement at runtime. Here's an example:
81+
82+
```ts
83+
// window-service.ts
84+
import {Injectable} from '@angular/core';
85+
86+
@Injectable()
87+
export class WindowService {
88+
getWidth(): number {
89+
return window.innerWidth;
90+
}
91+
}
92+
```
93+
94+
```ts
95+
// server-window.service.ts
96+
import {Injectable} from '@angular/core';
97+
import {WindowService} from './window.service';
98+
99+
@Injectable()
100+
export class ServerWindowService extends WindowService {
101+
getWidth(): number {
102+
return 0;
103+
}
104+
}
105+
```
106+
107+
```ts
108+
// app-server.module.ts
109+
import {NgModule} from '@angular/core';
110+
import {WindowService} from './window.service';
111+
import {ServerWindowService} from './server-window.service';
112+
113+
@NgModule({
114+
providers: [{
115+
provide: WindowService,
116+
useClass: ServerWindowService,
117+
}]
118+
})
119+
```
120+
121+
If you have a component provided by a third-party that is not Universal-compatible out of the box,
122+
you can create two separate modules for browser and server (the server module you should already have),
123+
in addition to your base app module. The base app module will contain all of your platform-agnostic code,
124+
the browser module will contain all of your browser-specific/server-incompatible code, and vice-versa for
125+
your server module. In order to avoid editing too much template code, you can create a no-op component
126+
to drop in for the library component. Here's an example:
127+
128+
```ts
129+
// example.component.ts
130+
import {Component} from '@angular/core';
131+
132+
@Component({
133+
selector: 'example-component',
134+
template: `<library-component></library-component>` // this is provided by a third-party lib
135+
// that causes issues rendering on Universal
136+
})
137+
export class ExampleComponent {
138+
}
139+
```
140+
141+
```ts
142+
// app.module.ts
143+
import {NgModule} from '@angular/core';
144+
import {ExampleComponent} from './example.component';
145+
146+
@NgModule({
147+
declarations: [ExampleComponent],
148+
})
149+
```
150+
151+
```ts
152+
// browser-app.module.ts
153+
import {NgModule} from '@angular/core';
154+
import {LibraryModule} from 'some-lib';
155+
import {AppModule} from './app.module';
156+
157+
@NgModule({
158+
imports: [AppModule, LibraryModule],
159+
})
160+
```
161+
162+
```ts
163+
// library-shim.component.ts
164+
import {Component} from '@angular/core';
165+
166+
@Component({
167+
selector: 'library-component',
168+
template: ''
169+
})
170+
export class LibraryShimComponent {
171+
}
172+
```
173+
174+
```ts
175+
// server.app.module.ts
176+
import {NgModule} from '@angular/core';
177+
import {LibraryShimComponent} from './library-shim.component';
178+
import {AppModule} from './app.module';
179+
180+
@NgModule({
181+
imports: [AppModule],
182+
declarations: [LibraryShimComponent],
183+
})
184+
export class ServerAppModule {
32185
}
33186
```
34-
- The application runs XHR requests on the server & once again on the Client-side (when the application bootstraps)
35-
- Use a cache that's transferred from server to client (TODO: Point to the example)
36-
- Know the difference between attributes and properties in relation to the DOM.
37-
- 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.
187+
188+
#### Strategy 3: Shims
189+
190+
If all else fails, and you simply must have access to some sort of browser functionality, you can patch
191+
the global scope of the server environment to include the globals you need. For instance:
192+
193+
```ts
194+
// server.ts
195+
global['window'] = {
196+
// properties you need implemented here...
197+
};
198+
```
199+
200+
This can be applied to any undefined element. Please be careful when you do this, as playing with the global
201+
scope is generally considered an anti-pattern.
202+
203+
> Fun fact: a shim is a patch for functionality that will never be supported on a given platform. A
204+
> polyfill is a patch for functionality that is planned to be supported, or is supported on newer versions
205+
206+
## Application is slow, or worse, won't render
207+
208+
The Angular Universal rendering process is straightforward, but just as simply can be blocked or slowed down
209+
by well-meaning or innocent-looking code. First, some background on the rendering process. When a render
210+
request is made for platform-server (the Angular Universal platform), a single route navigation is executed.
211+
When that navigation completes, meaning that all Zone.js macrotasks are completed, the DOM in whatever state
212+
it's in at that time is returned to the user.
213+
214+
> A Zone.js macrotask is just a JavaScript macrotask that executes in/is patched by Zone.js
215+
216+
This means that if there is a process, like a microtask, that takes up ticks to complete, or a long-standing
217+
HTTP request, the rendering process will not complete, or will take longer. Macrotasks include calls to globals
218+
like `setTimeout` and `setInterval`, and `Observables`. Calling these without cancelling them, or letting them run
219+
longer than needed on the server could result in suboptimal rendering.
220+
221+
> It may be worth brushing up on the JavaScript event loop and learning the difference between microtasks
222+
> and macrotasks, if you don't know it already. [Here's](https://javascript.info/event-loop) a good reference.
223+
224+
## My X, Y, Z won't finish before render!
225+
226+
Similarly to the above section on waiting for macrotasks to complete, the flip-side is that the platform will
227+
not wait for microtasks to complete before finishing the render. In Angular Universal, we have patched the
228+
Angular HTTP client to turn it into a macrotask, to ensure that any needed HTTP requests complete for a given
229+
render. However, this type of patch may not be appropriate for all microtasks, and so it is recommended you use
230+
your best judgment on how to proceed. You can look at the code reference for how Universal wraps a task to turn
231+
it into a macrotask, or you can simply opt to change the server behavior of the given tasks.

0 commit comments

Comments
 (0)