Angular Universal с Contentful и TransferState
У меня настоящая проблема с довольным и угловатым универсалом. Я пробовал так много вещей, чтобы заставить контент загружаться и отображаться в View Page Source, но все, что я пробовал, не удалось. Самое близкое, что у меня есть, - это использование TransferState, которое я нашел в учебнике, посвященном вызовам API.
Я создал этот класс:
import { Injectable } from '@angular/core';
import { TransferState } from '@angular/platform-browser';
import { Observable, from } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class TransferHttpService {
constructor(
private transferHttp: TransferState,
private httpClient: HttpClient
) {}
get(url, options?): Observable<any> {
return this.getData(url, options, () => {
return this.httpClient.get(url, options);
});
}
post(url, body, options?): Observable<any> {
return this.getData(url, options, () => {
return this.httpClient.post(url, body, options);
});
}
delete(url, options?): Observable<any> {
return this.getData(url, options, () => {
return this.httpClient.delete(url, options);
});
}
put(url, body, options?): Observable<any> {
return this.getData(url, options, () => {
return this.httpClient.put(url, body, options);
});
}
getData(url, options, callback: () => Observable<any>): Observable<any> {
const optionsString = options ? JSON.stringify(options) : '';
let key = `${url + optionsString}`;
try {
return this.resolveData(key);
} catch (e) {
console.log('In catch', key);
return callback().pipe(
tap((data) => {
console.log('cache set', key);
this.setCache(key, data);
})
);
}
}
resolveData(key) {
let resultData: any;
if (this.hasKey(key)) {
resultData = this.getFromCache(key);
console.log('got cache', key);
} else {
throw new Error();
}
return from(Promise.resolve(resultData));
}
setCache(key, value) {
this.transferHttp.set(key, value);
}
getFromCache(key) {
return this.transferHttp.get(key, null); // null set as default value
}
hasKey(key) {
return this.transferHttp.hasKey(key);
}
}
А потом куда я ввел HttpClient
, Я заменил его новым классом (TransferHttpService
), который отлично работает. При использовании contentful я обновил свой старый сервис до следующего:
import { Injectable } from '@angular/core';
import { createClient, Entry } from 'contentful';
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
import { Observable, from } from 'rxjs';
import { environment } from '@environments/environment';
import { Content, Page, Menu, MenuItem, Footer, Image } from '@models';
import { TransferHttpService } from './transfer-http.service';
@Injectable({
providedIn: 'root',
})
export class ContentfulService {
private client = createClient({
space: environment.space,
accessToken: environment.cdaAccessToken,
});
constructor(private http: TransferHttpService) {}
getMetadata(): Observable<any> {
return this.http.getData('metadata', null, () => {
return from(
this.client
.getEntries({
content_type: 'metadata',
})
.then((response) => response.items[0])
);
});
}
getPages(): Observable<Page[]> {
return this.http.getData('pages', null, () => {
return from(
this.client
.getEntries({
content_type: 'page',
include: 3,
})
.then((response) => {
let pages = response.items.map((item) =>
this.createPage(item, this.createContent)
);
return pages;
})
);
});
}
getNavigation(): Observable<Menu> {
return this.http.getData('navigation', null, () => {
return from(
this.client
.getEntries({
content_type: 'navigationMenu',
})
.then((response) => this.createMenu(response.items[0]))
);
});
}
getFooter(): Observable<Footer> {
return this.http.getData('footer', null, () => {
return from(
this.client
.getEntries({
content_type: 'footer',
include: 2,
})
.then((response) => this.createFooter(response.items[0]))
);
});
}
public createImage(component: any): Image {
if (!component) return;
return {
title: component.fields.title,
url: component.fields.file.url,
alt: component.fields.file.fileName,
};
}
private createFooter(component: any): Footer {
return {
title: component.fields.title,
htmlContent: documentToHtmlString(component.fields.content),
careersText: component.fields.careersText,
careersLink: component.fields.careersLink,
cookiePolicyLink: component.fields.cookiePolicyLink,
privacyPolicyLink: component.fields.privacyPolicyLink,
termsLink: component.fields.termsLink,
};
}
private createMenu(menu: any): Menu {
return {
title: menu.fields.title,
links: menu.fields.links.map(this.createMenuItem),
};
}
private createMenuItem(item: any): MenuItem {
return {
path: item.fields.path,
text: item.fields.linkText,
};
}
private createPage(page: Entry<any>, createContent: any): Page {
return {
title: page.fields['title'],
slug: page.fields['path'],
linkText: page.fields['linkText'],
content: page.fields['content'].map(createContent),
};
}
private createContent(component: Entry<any>): Content {
return {
type: component.sys.contentType.sys.id,
fields: component.fields,
};
}
}
Как видите, я выполняю createClient getEntries
обещание наблюдаемому после того, как я обработал ответ, который затем передаю TransferHttpService
к getData
метод, который отвечает за установку / получение кешированного ответа.
Я подозреваю, что проблема в этом, но кажется, что верхний и нижний колонтитулы всегда кэшируются.
Я играл с этим, и я получил лучшие результаты, когда изменил свой TransferHttpService на это:
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import {
TransferState,
StateKey,
makeStateKey,
} from '@angular/platform-browser';
import { Observable, from } from 'rxjs';
import { tap } from 'rxjs/operators';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class TransferHttpService {
constructor(
protected transferState: TransferState,
private httpClient: HttpClient,
@Inject(PLATFORM_ID) private platformId: Object
) {}
request<T>(
method: string,
uri: string | Request,
options?: {
body?: any;
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
reportProgress?: boolean;
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
method,
uri,
options,
(method: string, url: string, options: any) => {
return this.httpClient.request<T>(method, url, options);
}
);
}
/**
* Performs a request with `get` http method.
*/
get<T>(
url: string,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
'get',
url,
options,
(_method: string, url: string, options: any) => {
return this.httpClient.get<T>(url, options);
}
);
}
/**
* Performs a request with `post` http method.
*/
post<T>(
url: string,
body: any,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getPostData<T>(
'post',
url,
body,
options,
// tslint:disable-next-line:no-shadowed-variable
(_method: string, url: string, body: any, options: any) => {
return this.httpClient.post<T>(url, body, options);
}
);
}
/**
* Performs a request with `put` http method.
*/
put<T>(
url: string,
_body: any,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'body';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getPostData<T>(
'put',
url,
_body,
options,
(_method: string, url: string, _body: any, options: any) => {
return this.httpClient.put<T>(url, _body, options);
}
);
}
/**
* Performs a request with `delete` http method.
*/
delete<T>(
url: string,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
'delete',
url,
options,
(_method: string, url: string, options: any) => {
return this.httpClient.delete<T>(url, options);
}
);
}
/**
* Performs a request with `patch` http method.
*/
patch<T>(
url: string,
body: any,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getPostData<T>(
'patch',
url,
body,
options,
// tslint:disable-next-line:no-shadowed-variable
(
_method: string,
url: string,
body: any,
options: any
): Observable<any> => {
return this.httpClient.patch<T>(url, body, options);
}
);
}
/**
* Performs a request with `head` http method.
*/
head<T>(
url: string,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
'head',
url,
options,
(_method: string, url: string, options: any) => {
return this.httpClient.head<T>(url, options);
}
);
}
/**
* Performs a request with `options` http method.
*/
options<T>(
url: string,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
'options',
url,
options,
// tslint:disable-next-line:no-shadowed-variable
(_method: string, url: string, options: any) => {
return this.httpClient.options<T>(url, options);
}
);
}
// tslint:disable-next-line:max-line-length
getData<T>(
method: string,
uri: string | Request,
options: any,
callback: (
method: string,
uri: string | Request,
options: any
) => Observable<any>
): Observable<T> {
let url = uri;
if (typeof uri !== 'string') {
url = uri.url;
}
const tempKey = url + (options ? JSON.stringify(options) : '');
const key = makeStateKey<T>(tempKey);
try {
return this.resolveData<T>(key);
} catch (e) {
console.log('in catch', key);
return callback(method, uri, options).pipe(
tap((data: T) => {
if (isPlatformBrowser(this.platformId)) {
// Client only code.
// nothing;
}
if (isPlatformServer(this.platformId)) {
console.log('set cache', key);
this.setCache<T>(key, data);
}
})
);
}
}
private getPostData<T>(
_method: string,
uri: string | Request,
body: any,
options: any,
callback: (
method: string,
uri: string | Request,
body: any,
options: any
) => Observable<any>
): Observable<T> {
let url = uri;
if (typeof uri !== 'string') {
url = uri.url;
}
const tempKey =
url +
(body ? JSON.stringify(body) : '') +
(options ? JSON.stringify(options) : '');
const key = makeStateKey<T>(tempKey);
try {
return this.resolveData<T>(key);
} catch (e) {
return callback(_method, uri, body, options).pipe(
tap((data: T) => {
if (isPlatformBrowser(this.platformId)) {
// Client only code.
// nothing;
}
if (isPlatformServer(this.platformId)) {
this.setCache<T>(key, data);
}
})
);
}
}
private resolveData<T>(key: StateKey<T>): Observable<T> {
const data = this.getFromCache<T>(key);
console.log(data);
if (!data) {
throw new Error();
}
if (isPlatformBrowser(this.platformId)) {
console.log('get cache', key);
// Client only code.
this.transferState.remove(key);
}
if (isPlatformServer(this.platformId)) {
console.log('we are the server');
// Server only code.
}
return from(Promise.resolve<T>(data));
}
private setCache<T>(key: StateKey<T>, data: T): void {
return this.transferState.set<T>(key, data);
}
private getFromCache<T>(key: StateKey<T>): T {
return this.transferState.get<T>(key, null);
}
}
потратив на это несколько часов, я обнаружил, что контент работает только тогда, когда есть вызов API на мой сервер, что странно...
Ах! Клянусь, это не должно быть так сложно. Если у меня есть такой компонент:
import { Component, OnInit } from '@angular/core';
import { Page } from '@models';
import { ContentfulService, TransferHttpService } from '@services';
import { environment } from '@environments/environment';
@Component({
templateUrl: './pages.component.html',
styleUrls: ['./pages.component.scss'],
})
export class PagesComponent implements OnInit {
public pages: Page[];
constructor(
private brandService: TransferHttpService,
private contentfulService: ContentfulService
) {}
ngOnInit(): void {
this.listPages();
this.listBrands();
}
private listPages(): void {
this.contentfulService
.getPages()
.subscribe((pages: any) => (this.pages = pages));
}
private listBrands(): void {
this.brandService
.get(`${environment.apiUrl}/brands/simple`)
.subscribe((response: any) => {
console.log(response);
});
}
}
когда я запускаю свой локальный сервер npm run dev:ssr
Я получаю это в консоли:
Что идеально. Все находится в моем исходном коде страницы просмотра. НО! Если я удалюlistBrands
метод (потому что он не нужен, я просто использовал его как тест). Я получаю это:
Что нехорошо; ничего не кэшируется, и у моего источника страницы просмотра нет верхнего, нижнего колонтитула, страницы или метаданных...
Я не понимаю, что происходит. Почему это работает, когда у меня есть еще один вызов API на другой сервер?
1 ответ
Мне удалось это понять, контентное приложение использует HttpClient, но дает вам возможность использовать адаптер. Здесь упоминаются аксиомы, но я действительно не знаю, что это такое. Я потратил несколько часов на игры и обнаружил, что могу использовать свой собственныйTransferHttpService
в адаптере вот так:
private client = createClient({
space: environment.space,
accessToken: environment.cdaAccessToken,
adapter: async (config: any) => {
config.adapter = null;
const response = await this.http
.request(config.method, `${config.baseURL}/${config.url}`, {
headers: {
Accept: config.headers['Accept'],
Authorization: config.headers['Authorization'],
'Content-Type': config.headers['Content-Type'],
'X-Contentful-User-Agent':
config.headers['X-Contentful-User-Agent'],
},
params: config.params,
})
.toPromise();
return {
data: response,
};
},
});
В TransferHttpService
выглядит так:
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import {
TransferState,
StateKey,
makeStateKey,
} from '@angular/platform-browser';
import { Observable, from } from 'rxjs';
import { tap } from 'rxjs/operators';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class TransferHttpService {
constructor(
protected transferState: TransferState,
private httpClient: HttpClient,
@Inject(PLATFORM_ID) private platformId: Object
) {}
request<T>(
method: string,
uri: string | Request,
options?: {
body?: any;
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
reportProgress?: boolean;
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
method,
uri,
options,
(method: string, url: string, options: any) => {
return this.httpClient.request<T>(method, url, options);
}
);
}
/**
* Performs a request with `get` http method.
*/
get<T>(
url: string,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
'get',
url,
options,
(_method: string, url: string, options: any) => {
return this.httpClient.get<T>(url, options);
}
);
}
/**
* Performs a request with `post` http method.
*/
post<T>(
url: string,
body: any,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getPostData<T>(
'post',
url,
body,
options,
// tslint:disable-next-line:no-shadowed-variable
(_method: string, url: string, body: any, options: any) => {
return this.httpClient.post<T>(url, body, options);
}
);
}
/**
* Performs a request with `put` http method.
*/
put<T>(
url: string,
_body: any,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'body';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getPostData<T>(
'put',
url,
_body,
options,
(_method: string, url: string, _body: any, options: any) => {
return this.httpClient.put<T>(url, _body, options);
}
);
}
/**
* Performs a request with `delete` http method.
*/
delete<T>(
url: string,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
'delete',
url,
options,
(_method: string, url: string, options: any) => {
return this.httpClient.delete<T>(url, options);
}
);
}
/**
* Performs a request with `patch` http method.
*/
patch<T>(
url: string,
body: any,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getPostData<T>(
'patch',
url,
body,
options,
// tslint:disable-next-line:no-shadowed-variable
(
_method: string,
url: string,
body: any,
options: any
): Observable<any> => {
return this.httpClient.patch<T>(url, body, options);
}
);
}
/**
* Performs a request with `head` http method.
*/
head<T>(
url: string,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
'head',
url,
options,
(_method: string, url: string, options: any) => {
return this.httpClient.head<T>(url, options);
}
);
}
/**
* Performs a request with `options` http method.
*/
options<T>(
url: string,
options?: {
headers?:
| HttpHeaders
| {
[header: string]: string | string[];
};
observe?: 'response';
params?:
| HttpParams
| {
[param: string]: string | string[];
};
reportProgress?: boolean;
responseType?: 'json';
withCredentials?: boolean;
}
): Observable<T> {
// tslint:disable-next-line:no-shadowed-variable
return this.getData<T>(
'options',
url,
options,
// tslint:disable-next-line:no-shadowed-variable
(_method: string, url: string, options: any) => {
return this.httpClient.options<T>(url, options);
}
);
}
// tslint:disable-next-line:max-line-length
getData<T>(
method: string,
uri: string | Request,
options: any,
callback: (
method: string,
uri: string | Request,
options: any
) => Observable<any>
): Observable<T> {
let url = uri;
if (typeof uri !== 'string') {
url = uri.url;
}
const tempKey = url + (options ? JSON.stringify(options) : '');
const key = makeStateKey<T>(tempKey);
try {
return this.resolveData<T>(key);
} catch (e) {
//console.log('in catch', key);
return callback(method, uri, options).pipe(
tap((data: T) => {
if (isPlatformBrowser(this.platformId)) {
// Client only code.
// nothing;
}
if (isPlatformServer(this.platformId)) {
//console.log('set cache', key);
this.setCache<T>(key, data);
}
})
);
}
}
private getPostData<T>(
_method: string,
uri: string | Request,
body: any,
options: any,
callback: (
method: string,
uri: string | Request,
body: any,
options: any
) => Observable<any>
): Observable<T> {
let url = uri;
if (typeof uri !== 'string') {
url = uri.url;
}
const tempKey =
url +
(body ? JSON.stringify(body) : '') +
(options ? JSON.stringify(options) : '');
const key = makeStateKey<T>(tempKey);
try {
return this.resolveData<T>(key);
} catch (e) {
return callback(_method, uri, body, options).pipe(
tap((data: T) => {
if (isPlatformBrowser(this.platformId)) {
// Client only code.
// nothing;
}
if (isPlatformServer(this.platformId)) {
this.setCache<T>(key, data);
}
})
);
}
}
private resolveData<T>(key: StateKey<T>): Observable<T> {
const data = this.getFromCache<T>(key);
if (!data) {
throw new Error();
}
if (isPlatformBrowser(this.platformId)) {
//console.log('get cache', key);
// Client only code.
this.transferState.remove(key);
}
if (isPlatformServer(this.platformId)) {
//console.log('we are the server');
// Server only code.
}
return from(Promise.resolve<T>(data));
}
private setCache<T>(key: StateKey<T>, data: T): void {
return this.transferState.set<T>(key, data);
}
private getFromCache<T>(key: StateKey<T>): T {
return this.transferState.get<T>(key, null);
}
}
Вот и все; как только я это сделал, все начало работать.