Skip to content

Commit 33e0db6

Browse files
authored
use state transfer for ssr (#241)
## Why? - During Angular universal application loading, the API requests were running even if the data was already rendered from the server. - The router was flickering on angular universal first page load. - Product detail page shown as blank when data is fetching from api. ## This change addresses the need by: - added state transfer interceptor to check for stored data in state and avoid api calls if data already sent from SSR server - Fixed route resolve issue for product detail - Bug fix for app route flicker on universal boot. Fix shown in this [angular Issue.](angular/angular#15716 (comment))
1 parent 92cfce5 commit 33e0db6

File tree

68 files changed

+379
-260
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+379
-260
lines changed

.firebaserc

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
"default": "angularspree",
55
"staging": "angularspree"
66
}
7-
}
7+
}

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"node_modules": true,
99
"bin": true,
1010
"obj": true,
11-
"dist": true,
11+
"dist": false,
1212
"Properties": true,
1313
".vs": true,
1414
"*.csproj": true,

angular.json

+5-7
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,20 @@
2626
"input": "src",
2727
"output": "/"
2828
},
29-
{
30-
"glob": "service-worker.js",
31-
"input": "src",
32-
"output": "/"
33-
},
3429
{
3530
"glob": "manifest.json",
3631
"input": "src",
3732
"output": "/"
3833
},
34+
"src/manifest.json",
3935
"src/manifest.json"
4036
],
4137
"styles": [
4238
"node_modules/font-awesome/css/font-awesome.css",
4339
"src/styles.scss"
4440
],
4541
"scripts": [],
46-
"serviceWorker": true
42+
"serviceWorker": false
4743
},
4844
"configurations": {
4945
"mock-ng-spree": {
@@ -97,6 +93,7 @@
9793
"extractLicenses": true,
9894
"vendorChunk": false,
9995
"buildOptimizer": true,
96+
"serviceWorker": false,
10097
"fileReplacements": [
10198
{
10299
"replace": "src/environments/environment.ts",
@@ -218,6 +215,7 @@
218215
"input": "src",
219216
"output": "/"
220217
},
218+
"src/manifest.json",
221219
"src/manifest.json"
222220
]
223221
}
@@ -271,4 +269,4 @@
271269
"prefix": "app"
272270
}
273271
}
274-
}
272+
}

ngsw-config.json

+35-20
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
{
22
"index": "/index.html",
3-
"assetGroups": [{
4-
"name": "app",
5-
"installMode": "prefetch",
6-
"resources": {
7-
"files": [
8-
"/favicon.ico",
9-
"/index.html",
10-
"/*.css",
11-
"/*.js"
12-
]
3+
"dataGroups": [
4+
{
5+
"name": "api-performance",
6+
"urls": [
7+
"/products"
8+
],
9+
"cacheConfig": {
10+
"strategy": "freshness",
11+
"maxSize": 100,
12+
"maxAge": "3d"
13+
}
1314
}
14-
}, {
15-
"name": "assets",
16-
"installMode": "lazy",
17-
"updateMode": "prefetch",
18-
"resources": {
19-
"files": [
20-
"/assets/**"
21-
]
15+
],
16+
"assetGroups": [
17+
{
18+
"name": "app",
19+
"installMode": "prefetch",
20+
"resources": {
21+
"files": [
22+
"/favicon.ico",
23+
"/index.html",
24+
"/*.css",
25+
"/*.js"
26+
]
27+
}
28+
}, {
29+
"name": "assets",
30+
"installMode": "lazy",
31+
"updateMode": "prefetch",
32+
"resources": {
33+
"files": [
34+
"/assets/**"
35+
]
36+
}
2237
}
23-
}]
24-
}
38+
]
39+
}

package.json

+3-15
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,29 @@
44
"license": "MIT",
55
"scripts": {
66
"ng": "./node_modules/@angular/cli/bin/ng",
7-
87
"start:mock-ng-spree": "ng serve --configuration=mock-ng-spree",
9-
108
"e2e:dev-ng-spree": "ng e2e --configuration=dev-ng-spree",
119
"lint:dev-ng-spree": "ng lint --configuration=dev-ng-spree",
1210
"test:dev-ng-spree": "ng test --configuration=dev-ng-spree",
1311
"build:dev-ng-spree": "ng build --configuration=dev-ng-spree",
1412
"start:dev-ng-spree": "ng serve --configuration=dev-ng-spree",
1513
"build:prod-ng-spree": "ng build --configuration=prod-ng-spree",
16-
17-
1814
"e2e:dev-custom": "ng e2e --configuration=dev-custom",
1915
"lint:dev-custom": "ng lint --configuration=dev-custom",
2016
"test:dev-custom": "ng test --configuration=dev-custom",
2117
"build:dev-custom": "ng build --configuration=dev-custom",
2218
"start:dev-custom": "ng serve --configuration=dev-custom",
2319
"build:prod-custom": "ng build --configuration=prod-custom",
24-
2520
"compodoc": "./node_modules/.bin/compodoc -p src/tsconfig.app.json -d docs/",
26-
2721
"sw": "sw-precache --root=dist --config=sw-precache-config.js",
28-
2922
"bundle-report": "webpack-bundle-analyzer dist/stats.json",
30-
3123
"static-serve": "cd dist/browser && live-server --port=4200 --host=localhost --entry-file=/index.html",
32-
3324
"build:ssr:prod-custom": "npm run build:client-and-server-bundles:prod-custom && npm run webpack:server",
3425
"build:ssr:prod-ng-spree": "npm run build:client-and-server-bundles:prod-ng-spree && npm run webpack:server",
35-
3626
"start:ssr:prod-custom": "npm run build:ssr:prod-custom && node dist/server",
3727
"start:ssr:prod-ng-spree": "npm run build:ssr:prod-ng-spree && node dist/server",
38-
3928
"build:client-and-server-bundles:prod-custom": "npm run build:prod-custom && ng run angularspree:server:prod-custom",
4029
"build:client-and-server-bundles:prod-ng-spree": "npm run build:prod-ng-spree && ng run angularspree:server:prod-ng-spree",
41-
4230
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
4331
},
4432
"private": true,
@@ -52,7 +40,7 @@
5240
"@angular/platform-browser": "^6.1.1",
5341
"@angular/platform-browser-dynamic": "^6.1.1",
5442
"@angular/platform-server": "^6.1.1",
55-
"@angular/pwa": "^0.7.2",
43+
"@angular/pwa": "^0.7.4",
5644
"@angular/router": "^6.1.1",
5745
"@angular/service-worker": "^6.1.1",
5846
"@ngrx/core": "^1.2.0",
@@ -89,8 +77,8 @@
8977
"zone.js": "^0.8.26"
9078
},
9179
"devDependencies": {
92-
"@angular-devkit/build-angular": "^0.7.2",
93-
"@angular/cli": "^6.1.2",
80+
"@angular-devkit/build-angular": "^0.7.4",
81+
"@angular/cli": "^6.1.4",
9482
"@angular/compiler-cli": "^6.1.1",
9583
"@angular/language-service": "^6.1.1",
9684
"@angularclass/hmr": "^2.1.3",

src/app/app.module.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { ToastrModule } from 'ngx-toastr';
3737
CheckoutFooterComponent
3838
],
3939
imports: [
40-
RouterModule.forRoot(routes, { preloadingStrategy: AppPreloadingStrategy }),
40+
RouterModule.forRoot(routes, { preloadingStrategy: AppPreloadingStrategy, initialNavigation: 'enabled' }),
4141
StoreModule.forRoot(reducers, { metaReducers }),
4242

4343
/**
@@ -77,7 +77,7 @@ import { ToastrModule } from 'ngx-toastr';
7777
}),
7878
CoreModule,
7979
SharedModule,
80-
ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production })
80+
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
8181
],
8282
providers: [AppPreloadingStrategy],
8383
bootstrap: [AppComponent]

src/app/checkout/cart/cart.module.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { EffectsModule } from '@ngrx/effects';
21
import { CommonModule } from '@angular/common';
32
import { CartComponent } from './cart.component';
43
import { NgModule } from '@angular/core';

src/app/core/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { TransferStateInterceptor } from './interceptors/transfer-state.interceptor';
2+
import { TransferStateService } from './services/transfer-state.service';
13
import { CheckoutEffects } from './../checkout/effects/checkout.effects';
24
import { CheckoutActions } from './../checkout/actions/checkout.actions';
35
import { CheckoutService } from './services/checkout.service';
@@ -63,6 +65,8 @@ import { CanActivateViaAuthGuard } from './guards/auth.guard';
6365
UserActions,
6466
UserService,
6567
CanActivateViaAuthGuard,
68+
{provide: HTTP_INTERCEPTORS, useClass: TransferStateInterceptor, multi: true},
69+
TransferStateService,
6670
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },
6771
]
6872
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { tap } from 'rxjs/operators';
2+
import { Observable, of } from 'rxjs';
3+
import { Injectable } from '@angular/core';
4+
import {
5+
HttpRequest,
6+
HttpHandler,
7+
HttpEvent,
8+
HttpInterceptor,
9+
HttpResponse
10+
} from '@angular/common/http';
11+
import { TransferStateService } from '../services/transfer-state.service';
12+
13+
@Injectable()
14+
export class TransferStateInterceptor implements HttpInterceptor {
15+
constructor(private transferStateService: TransferStateService) {
16+
}
17+
18+
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
19+
/**
20+
* Skip this interceptor if the request method isn't GET.
21+
*/
22+
if (req.method !== 'GET') {
23+
return next.handle(req);
24+
}
25+
26+
const cachedResponse = this.transferStateService.getCache(req.url);
27+
if (cachedResponse) {
28+
// A cached response exists which means server set it before. Serve it instead of forwarding
29+
// the request to the next handler.
30+
return of(new HttpResponse<any>({ body: cachedResponse }));
31+
}
32+
33+
/**
34+
* No cached response exists. Go to the network, and cache
35+
* the response when it arrives.
36+
*/
37+
return next.handle(req).pipe(
38+
tap(event => {
39+
if (event instanceof HttpResponse) {
40+
this.transferStateService.setCache(req.url, event.body);
41+
}
42+
})
43+
);
44+
}
45+
}

src/app/core/services/auth.service.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { of as observableOf, Observable } from 'rxjs';
1+
import { of as observableOf, Observable, throwError, of } from 'rxjs';
22
import { catchError, map, tap } from 'rxjs/operators';
33
import { Router } from '@angular/router';
44
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
@@ -30,7 +30,7 @@ export class AuthService {
3030
private toastrService: ToastrService,
3131
private router: Router,
3232
@Inject(PLATFORM_ID) private platformId: any
33-
) {}
33+
) { }
3434

3535
/**
3636
*
@@ -141,11 +141,7 @@ export class AuthService {
141141
authorized(): Observable<any> {
142142
return this.http
143143
.get('auth/authenticated')
144-
.pipe(map((res: Response) => res));
145-
// catch should be handled here with the http observable
146-
// so that only the inner obs dies and not the effect Observable
147-
// otherwise no further login requests will be fired
148-
// MORE INFO https://youtu.be/3LKMwkuK0ZE?t=24m29s
144+
.pipe(catchError(error => of(error.error)));
149145
}
150146

151147
/**

src/app/core/services/product.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class ProductService {
3636
getProduct(id: string): Observable<Product> {
3737
return this.http
3838
.get<{ data: CJsonApi }>(
39-
`api/v1/products/${id}?data_set=large&${+new Date()}`
39+
`api/v1/products/${id}?data_set=large&${+new Date().getDate()}`
4040
)
4141
.pipe(
4242
map(resp => {
@@ -148,7 +148,7 @@ export class ProductService {
148148
getproductsByKeyword(keyword: string): Observable<any> {
149149
return this.http
150150
.get<{ data: CJsonApi[]; pagination: Object }>(
151-
`api/v1/products?${keyword}&per_page=20&data_set=small&${+new Date()}`
151+
`api/v1/products?${keyword}&per_page=20&data_set=small&${+new Date().getDate()}`
152152
)
153153
.pipe(
154154
map(resp => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
2+
import { TransferState, makeStateKey } from '@angular/platform-browser';
3+
import { isPlatformBrowser } from '@angular/common';
4+
5+
/**
6+
* Keep caches (makeStateKey) into it in each `setCache` function call
7+
* @type {any[]}
8+
*/
9+
const transferStateCache: String[] = [];
10+
11+
@Injectable()
12+
export class TransferStateService {
13+
constructor(private transferState: TransferState,
14+
@Inject(PLATFORM_ID) private platformId: Object,
15+
// @Inject(APP_ID) private _appId: string
16+
) {
17+
}
18+
19+
/**
20+
* Set cache only when it's running on server
21+
* @param {string} key
22+
* @param data Data to store to cache
23+
*/
24+
setCache(key: string, data: any) {
25+
if (!isPlatformBrowser(this.platformId)) {
26+
transferStateCache[key] = makeStateKey<any>(key);
27+
this.transferState.set(transferStateCache[key], data);
28+
}
29+
}
30+
31+
32+
/**
33+
* Returns stored cache only when it's running on browser
34+
* @param {string} key
35+
* @returns {any} cachedData
36+
*/
37+
getCache(key: string): any {
38+
if (isPlatformBrowser(this.platformId)) {
39+
const cachedData: any = this.transferState['store'][key];
40+
/**
41+
* Delete the cache to request the data from network next time which is the
42+
* user's expected behavior
43+
*/
44+
delete this.transferState['store'][key];
45+
return cachedData;
46+
}
47+
}
48+
}

src/app/home/content/content-header/content-header.component.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ export class ContentHeaderComponent implements OnInit {
6161
}
6262

6363
ngOnInit() {
64-
if (window.screen.width <= 768) {
65-
if (isPlatformBrowser(this.platformId)) {
64+
if (isPlatformBrowser(this.platformId)) {
65+
if (window.screen.width <= 768) {
6666
this.screenWidth = window.screen.width;
6767
}
6868
}

src/app/home/home.routes.ts

-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ import { CategoryPageComponent } from './category-page/category-page.component';
44
export const HomeRoutes = [
55
{ path: 'search', component: HomeComponent },
66
{ path: 'c/:number', component: CategoryPageComponent},
7-
87
];

src/app/interfaces.ts

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { ProductState } from './product/reducers/product-state';
22
import { AuthState } from './auth/reducers/auth.state';
33
import { UserState } from './user/reducers/user.state';
44
import { CheckoutState } from './checkout/reducers/checkout.state';
5-
import { SearchState } from './home/reducers/search.state';
65

76
// This should hold the AppState interface
87
// Ideally importing all the substate for the application

src/app/landing/brands-page/brands-page.component.html

-5
This file was deleted.

0 commit comments

Comments
 (0)