Angular 通用内容和 TransferState
Angular Universal with Contentful and TransferState
我在使用 contentful 和 angular 通用软件时遇到了一些实际问题。我已经尝试了很多方法来尝试让内容加载并呈现在 View Page Source 中,但我尝试的一切都失败了。
我得到的最接近的是使用 TransferState,我在涵盖 API 调用的教程中找到了它。
我创建了这个 class:
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
的地方,我替换为我的新 class (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
promise 到一个可观察对象,然后我将其传递给 TransferHttpService
到负责 setting/getting 缓存响应的 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);
}
}
在这上面花了几个小时后,我发现 contentful 只有在 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 调用时它仍然有效?
我设法解决了这个问题,contentful 使用了 HttpClient,但确实为您提供了使用适配器的选项。它提到了 axios 但我真的不知道那是什么。我花了好几个小时玩弄,发现我可以像这样在适配器中使用我自己的 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);
}
}
就是这样;一旦我这样做了,一切都开始工作了。
我在使用 contentful 和 angular 通用软件时遇到了一些实际问题。我已经尝试了很多方法来尝试让内容加载并呈现在 View Page Source 中,但我尝试的一切都失败了。 我得到的最接近的是使用 TransferState,我在涵盖 API 调用的教程中找到了它。
我创建了这个 class:
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
的地方,我替换为我的新 class (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
promise 到一个可观察对象,然后我将其传递给 TransferHttpService
到负责 setting/getting 缓存响应的 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);
}
}
在这上面花了几个小时后,我发现 contentful 只有在 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 调用时它仍然有效?
我设法解决了这个问题,contentful 使用了 HttpClient,但确实为您提供了使用适配器的选项。它提到了 axios 但我真的不知道那是什么。我花了好几个小时玩弄,发现我可以像这样在适配器中使用我自己的 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);
}
}
就是这样;一旦我这样做了,一切都开始工作了。