SAP Spartacus Site Context 实现专题页面

网友投稿 603 2022-11-16

SAP Spartacus Site Context 实现专题页面

SAP Spartacus Site Context 实现专题页面

文章目录

​​site-context.module.ts​​

​​initSiteContextConfig​​​​forroot​​

​​config​​

​​config-loader/site-context-config-initializer.js​​

​​resolveConfig​​​​getConfig​​​​isCurrentBaseSite​​​​getUrlParams​​

​​site-context-config.ts (SiteContextConfig 类)​​​​context-config-utils.ts (两个工具函数)​​

​​getContextParameterValues​​​​getContextParameterDefault​​

​​default-site-context-config (defaultSiteContextConfigFactory)​​​​base-site-config-validator.ts​​

​​connectors​​

​​converters.ts​​​​site.adapter.ts ( SiteAdapter)​​​​site.connector.ts (SiteConnector)​​

​​events​​

​​site-context-event.module.ts​​​​site-context-event.builder.ts (SiteContextEventBuilder)​​

​​register 方法​​​​registerSetLanguage​​

​​facade (名称稍稍有点 confuse,里面的文件是 .service.ts)​​

​​site-context.interface.ts (SiteContext)​​​​base-site.service.ts (BaseSiteService)​​

​​StateWithSiteContext​​​​getActive​​​​getAll​​

​​currency.service.ts (CurrencyService)​​​​language.service.ts(LanguageService)​​

​​providers​​

​​context-ids.ts​​​​context-service-map.ts (ContextServiceMap)​​

​​serviceMapFactory​​

​​site-context-params-providers (siteContextParamsProviders)​​​​context-service-providers.ts​​

​​initializeContext​​

​​context-initializer-providers.ts​​

​​services​​

​​base-site-initializer.ts(BaseSiteInitializer)​​

​​initialize​​​​setFallbackValue​​​​setDefaultFromConfig​​

​​currency-state-persistence.service.ts (CurrencyStatePersistenceService)​​

​​initSync​​​​onRead​​

​​site-context-params.service.ts (SiteContextParamsService)​​

​​getContextParameters​​​​getUrlEncodingParameters​​​​getParamValues​​​​getSiteContextService​​

​​site-context-routes-handler.ts (SiteContextRoutesHandler)​​

​​init​​​​setContextParamsFromRoute​​​​subscribeChanges​​

Site Context 的实现位于 ​​core\src\​​ 文件夹之下。

实现总共分下列八大部分:

每个子文件夹内都有一个 ​​barrel file​​​,​​index.ts​​.

site-context.module.ts

绝大多数 import 都是 ​​site-context​​ 文件夹自身的子文件夹提供的:

上图第一行,从 ​​@angular/core​​​ 导入 ​​ModuleWithProviders​​,这是一个包装器:

将 NgModule 与provider关联的 NgModule 的包装器。不推荐使用没有泛型类型的用法。 该 Module 对外只暴露了两个方法:

initSiteContextConfig

注入值:

​​SiteContextConfigInitializer​​​​SiteContextConfig​​

如果 context 的 context 值被填充,那么就返回 SiteContextConfigInitializer 实例,否则返回 null.

第 27 行的 ​​|​​ 代表联合类型。

forRoot

提供 global provider:

这个 forRoot 方法在 base-core.module.ts 的 ​​imports​​ 区域被调用。

config

先查看 index.ts,从三个 TypeScript 文件导入:

export * from './config-loader/site-context-config-initializer';export * from './context-config-utils';export * from './site-context-config';

config-loader/site-context-config-initializer.js

还是先看其 import 部分,从 site-context 的上层目录,即 ​​core/src​​ 里导入了:

实现了 configInitializer 接口。

这个接口定义在 ​​config​​ 文件夹里,包含了字符串数组类型的 scopes 属性,以及 configFactory,后者是一个函数,返回 Config 的 Promise.

Config 是一个纯虚 class:

export abstract class Config {}

所以为了实现该接口,依次定义了 scopes 值为 ​​context​​​ 字符串常量,以及调用自己的 ​​resolveConfig​​ 方法,返回一个 promise 对象。

构造函数里注入的三个属性,都是从 ​​config​​ 里导入的。

resolveConfig

调用 baseSiteService,取得所有的 baseSite,然后调用第 40 行的 find 方法,返回满足 predicate 函数的 ​​单个​​ site 实例:

从错误消息 ​​Error: Cannot get base site config! Current url (${this.currentUrl}) doesn't match any of url patterns of any base sites.​​ 似乎能看出,第 40 行代码根据当前 url 来判断是否能 match 到某个定义好的 base site.

最后,在第 50 行的 map 里,执行 projection,将 baseSite 映射成其配置信息。

getConfig

就是把输入的 ​​source​​ 变量,类型为 BaseSite, 的对应属性返回。

isCurrentBaseSite

将第 79 行通过依赖注入得到的当前 url,​​currentUrl​​​ 同 site 的 ​​urlPatterns​​ 数组做比较,通过正则表达式实现。如果匹配,说明当前 url 代表的就是一个 base site.

getUrlParams

将 OCC 里用的 ​​Storefront​​​ 字面量,映射成 Spartacus 里使用的常量 ​​baseSite​​:

site-context-config.ts (SiteContextConfig 类)

导入了 ​​core/config​​​ 文件夹里的 Config 类,这个类是一个纯虚类,源代码 ​​export abstract class Config {}​​​。给其增添了 ​​context​​​ 字段。下图第 15 行是 TypeScript 的 Module Argumentation 语法,参考笔者这篇​​文章​​。

context-config-utils.ts (两个工具函数)

getContextParameterValues

工具方法,返回 ​​string[]​​, 避免出现 null

getContextParameterDefault

由此可知,返回 Context Parameter 默认参数的逻辑就是,数组里 index 为 0 的元素,就是默认参数。

default-site-context-config (defaultSiteContextConfigFactory)

只有一个 function,负责返回默认的 site context,全是硬编码的 currency 和 language:

这是一个工厂函数,用于 ​​SiteContextModule​​​ 的 ​​forRoot​​ 静态方法里,返回 global providers 定义:

使用工厂函数返回默认 site context 配置:

base-site-config-validator.ts

这个文件比较简单,就包含一个 validator 校验器,没什么复杂逻辑:

export function baseSiteConfigValidator(config: SiteContextConfig) { if (getContextParameterDefault(config, BASE_SITE_CONTEXT_ID) === undefined) { return 'Please configure context.parameters.baseSite before using storefront library!'; }}

检查是否配置了默认的 baseSite.

connectors

包含:

site.connector.tssite.adapter.tsconverters.ts

converters.ts

还是从 ​​site-context​​ 平级目录导入:

创建几个 normalizer 的 injection token:

convertor 负责把数据模型从后台格式转换成 UI 模式:

site.adapter.ts ( SiteAdapter)

是一个纯虚函数:

在 core 的 occ 文件夹里,SiteAdapter 被 OCCSiteAdapter 实现:

site.connector.ts (SiteConnector)

使用了通过构造函数注入的 SiteAdapter 对应的方法。

events

这里出现了第二个 module:

site-context-event.module.ts

空的 module:

site-context-event.builder.ts (SiteContextEventBuilder)

全部的导入信息:

这里注入了标准的 ActionSubject,来自 ​​@ngrx/store​​:

register 方法

protected register(): void { this.registerSetLanguage(); this.registerSetCurrency(); }

registerSetLanguage

facade (名称稍稍有点 confuse,里面的文件是 .service.ts)

Facade 层为外界消费者屏蔽了 Ngrx 层的复杂度。

包含四大导出:

site-context.interface.ts (SiteContext)

定义了一个接口:

import { Observable } from 'rxjs';export interface SiteContext { getAll(): Observable; getActive(): Observable; setActive(isocode: string);}

从 ​​getAll​​ 能推测出,对于某个 Context 而言,支持多个值,并且只有一个值能成为 active 值。

base-site.service.ts (BaseSiteService)

BaseSite 的字段列表,定义在 ​​site-context​​ 外层文件夹的 model 下面:

​​BaseSiteService​​​ 类实现了 SiteContext 接口,即需要实现 ​​getAll​​ 等三个方法。

构造函数,定义 Store 和 SiteContextConfig 两个依赖。后者在全局 config 对象上,增添了 context 和 urlParameters 两个字段。前者来自 ​​@ngrx/store​​,类型参数为:StateWithSiteContext.

StateWithSiteContext

StateWithSiteContext 是一个接口:

export const SITE_CONTEXT_FEATURE = 'siteContext';export interface StateWithSiteContext { [SITE_CONTEXT_FEATURE]: SiteContextState;}

SiteContextState 的定义:

export interface SiteContextState { languages: LanguagesState; currencies: CurrenciesState; baseSite: BaseSiteState;}

LanguagesState 的定义:

export interface LanguagesState { entities: LanguagesEntities; activeLanguage: string;}

体现了 Context 的数据结构,数据集和当前 active 值。

LanguagesEntities 定义:

export interface LanguagesEntities { [isocode: string]: Language;}

Language 类型可以看成 ABAP structure:

export interface Language { active?: boolean; isocode?: string; name?: string; nativeName?: string;}

getActive

从 global in-memory state 里取出标志位为 active 的 site:

逐一分析。

SiteContextSelectors.getActiveBaseSite:

首先,SiteContextSelectors 的来源:

import { SiteContextSelectors } from '../store/selectors/index';

查看 selectors 文件夹下的 ​​index.ts​​, 这里把 group selector 的全部内容导出,然后用 as 生成了一个别名,方便其他消费者调用。

再看 getActiveBaseSite 的定义,包含5个关键点。

对比其调用代码:

(1) MemoizedSelector,说明这是一个包含使用内存进行结果缓存的 Selector,与关键点 3 通过 ​​createSelector​​ 方法创建相吻合。

(2) StateWithSiteContext,这是 Selector 的类型参数,表明 State 的数据结构。这个参数同构造函数里注入 Store 时传入的类型参数相吻合。

(3) createSelector:

StateWithSiteContext 前文已经介绍过,是一个 feature State,这是所有 State 的起点。 我们的业务逻辑里,需要查询 ActiveBaseSite,必然要从这个起点出发。

export const SITE_CONTEXT_FEATURE = 'siteContext';export interface StateWithSiteContext { [SITE_CONTEXT_FEATURE]: SiteContextState;}

SiteContextState 的定义:

export interface SiteContextState { languages: LanguagesState; currencies: CurrenciesState; baseSite: BaseSiteState;}

因此,createSelector 需要两个参数,第一个参数是另一个新的 Selector,这个 Selector 负责从构造函数注入的 Store 的类型参数出发,返回 SiteContextState. 第二个参数是一个 projector,负责从 SiteContextState 投射出字符串类型的 ActiveBaseSiteID:

至此我们只剩第四个关键点没有讨论了,就是 ​​createSelector​​ 的第一个参数,另一个 Selector:

getAll

我在研究时,发现这个方法没有触发(为什么?)

currency.service.ts (CurrencyService)

CurrencyService 的 getAll 确实被调用了:

调用者:site-context-component.service.ts

SiteContextComponentServiceprojects\storefrontlib\cms-components\misc\site-context-selector\site-context-component.service.ts有待将来深入

language.service.ts(LanguageService)

逻辑同 BaseSite 和 Currency.

providers

index.ts 里只有两份 export:

context-ids.ts

是一些参数常量。

export const LANGUAGE_CONTEXT_ID = 'language';export const CURRENCY_CONTEXT_ID = 'currency';export const BASE_SITE_CONTEXT_ID = 'baseSite';export const THEME_CONTEXT_ID = 'theme';

context-service-map.ts (ContextServiceMap)

ContextServiceMap 是一个 map 结构,key 是字符串,value 是这种 site context 对应的 SiteContext Facade 类?

比如我们之前在 ​​facade​​ 文件夹里讨论过的:

@Injectable()export class CurrencyService implements SiteContext { constructor( protected store: Store, protected config: SiteContextConfig ) {}

serviceMapFactory

工厂函数,负责提供 ContextServiceMap 的实现?

什么时候 ContextServiceMap 的实例会被调用到?

这里只有类的定义,并没有类的实例化过程:

site-context-params-providers (siteContextParamsProviders)

这个类没有被 index.ts 导出。从注释也能看出,这是有意为之,不将其暴露到 public API 去。

import { Provider } from '@angular/core';import { UrlSerializer } from '@angular/router';import { SiteContextParamsService } from '../services/site-context-params.service';import { SiteContextUrlSerializer } from '../services/site-context-url-serializer';// functions below should not be exposed in public API:export const siteContextParamsProviders: Provider[] = [ SiteContextParamsService, SiteContextUrlSerializer, { provide: UrlSerializer, useExisting: SiteContextUrlSerializer },];

提供了一些 provider 的实现,SiteContextUrlSerializer 提供了 Angular UrlSerializer 的实现。

context-service-providers.ts

initializeContext

这个函数就是一个包裹函数,从 site-context 平级的 config 文件夹里导入 ConfigInitializerService 和 site-context 内部的 service 文件夹内的 SiteContextRoutesHandler,然后调用这两个导入的函数的对应方法。

回答两个问题:

(1) initializeContext 函数什么时候被调用?

这个调用又分为两个维度来讨论。

第13行的代码,在 Angular 依赖注入框架内执行:

这比较合理,因为就在这个文件的尾部,​​initializeContext​​​ 作为 ​​APP_INITIALIZER​​ 被调用。

稍后,Angular APP_INITIALIZER 所有的 provider 也被调用:

(2) 这个函数的 23 行 init 方法执行了什么逻辑?

这个 init 方法,就是 ​​services​​ 问就按家里的实现,即通过 url 里包含的 language 和 currency 参数,设置 site context 的过程。

context-initializer-providers.ts

注册了 ​​APP_INITIALIZER​​ 的 provider.

services

currentcy 和 language,都有持久化。base site 没有。Service 里还包含通过 url 设置 context 的实现类。总共分 4 组实现。

base-site-initializer.ts(BaseSiteInitializer)

首先,这个类是 Injection Token ​​APP_INITIALIZER​​ 的 provider,在应用程序初始化时执行。

通过构造函数注入的参数,第一个参数来自 ​​facade​​​ 文件夹下的 Service 实现,第二个来自 site-context 同级的 ​​config​​ 文件夹。

@Injectable({ providedIn: 'root' })export class BaseSiteInitializer implements OnDestroy { constructor( protected baseSiteService: BaseSiteService, protected configInit: ConfigInitializerService ) {}

initialize

这个方法加载默认值,被 APP_INITIALIZER 的 provider 调用。

initialize(): void { this.subscription = this.configInit .getStable('context') .pipe( // TODO(#12351): <--- plug here explicitly SiteContextRoutesHandler switchMap(() => this.setFallbackValue()) ) .subscribe(); }

setFallbackValue

protected setFallbackValue(): Observable { return this.configInit .getStable('context') .pipe( tap((config: SiteContextConfig) => this.setDefaultFromConfig(config)) ); }

这里的 this.configInit.getStable(‘context’) 留待将来研究。

setDefaultFromConfig

从 config 的默认值设置 Active Base Site:

protected setDefaultFromConfig(config: SiteContextConfig): void { if (!this.baseSiteService.isInitialized()) { this.baseSiteService.setActive( getContextParameterDefault(config, BASE_SITE_CONTEXT_ID) ); } }

我们可以从调试器里观察一下运行时的行为:

所有的 APP_INITIALIZER provider 被调用,包括本章节正在介绍的 BaseSiteInitializer 的 initialize 方法:

subscribe 最终导致 setDefaultFromConfig 被调用:

此时 context 的所有值都已经就位了。

currency-state-persistence.service.ts (CurrencyStatePersistenceService)

注入的三个依赖:

export class CurrencyStatePersistenceService { constructor( protected statePersistenceService: StatePersistenceService, protected currencyService: CurrencyService, protected config: SiteContextConfig ) {}

(1) 来自 state 文件夹 (2) 来自 site-context facade 文件夹下 (3) 就是一个 abstract class,包含 urlParameters 属性,类型为 ​​​string[]​​​, 以及 ​​[contextName: string]: string[]​​

initSync

这个方法调用了 state 文件夹下的 StatePersistenceService,传递 key,state$ 和 onRead 三个参数。

关于 CurrencyService 的 isInitialized 方法,我加了 ​​config.log​​, 两次打印都是 true:

onRead

成功从 localstorage 里读取到了 USD,但是没有进入代码 29 行的 IF 分支,∵ currencyService.isInitialized 返回了 true:

所以这里没有执行 setActive 方法。setActive 方法是通过 url 解析而触发调用的:

site-context-params.service.ts (SiteContextParamsService)

该类通过构造函数注入的三个依赖,都是 ​​site-context​​ 文件夹内实现的资源。

SiteContextConfig 是在全局 config 对象上新增的 context 字段:

export abstract class SiteContextConfig { context?: { urlParameters?: string[]; [contextName: string]: string[] | undefined; };}

getContextParameters

该方法获得 context 里除了 urlParameters 之外的其他 context parameter,比如 language 和 currency 的字段名称。

getContextParameters(): string[] { if (this.config.context) { return Object.keys(this.config.context).filter( (param) => param !== 'urlParameters' ); } return []; }

getUrlEncodingParameters

取得 context 中的 url parameter 值:

getUrlEncodingParameters(): string[] { return (this.config.context && this.config.context.urlParameters) || []; }

getParamValues

取得参数的值内容:

getParamValues(param: string): string[] { return getContextParameterValues(this.config, param); }

getSiteContextService

根据参数名称,手动取得注入的参数服务类实例。在运行时,从浏览器地址栏 url 里,提取出地址栏包含的 url 参数:

解析出的 JSON 对象:

针对每个参数对象,根据参数名称,解析出对应的参数 service class,再调用 setValue 方法。

从 service map 里获得 service class 的 type,然后使用 Angular ​​injector​​ 进行实例注入:

将 active base site 设置成浏览器地址栏 url 里包含的值:

这个 base-site.service.ts 位于 facade 层:

另外,当 context 发生变化时,service class 的 getActive 也会发射最新的数据:

举个例子,从下拉列表里更改语言:

于是最新的语言,就被传入 subscribe 指定的回调函数里:

site-context-routes-handler.ts (SiteContextRoutesHandler)

三个依赖:

site context state 和 url 之间存在双向同步关系。

init

init() { this.router = this.injector.get(Router); this.location = this.injector.get(Location); const routingParams = this.siteContextParams.getUrlEncodingParameters(); if (routingParams.length) { this.setContextParamsFromRoute(this.location.path(true)); this.subscribeChanges(routingParams); this.subscribeRouting(); } }

上述代码,调用 ​​getUrlEncodingParameters​​ 获取参数列表:

然后从 this.location.path 得到当前浏览器地址栏里的 url,从 url ​​/electronics-spa/en/USD​​ 里提取出参数。

setContextParamsFromRoute

从 url 里解析参数,然后更新 site context state

解析出的参数是一个 JSON 对象:

根据参数名称,从 service map 里获取对应的 service 实例,然后调用 setValue 方法。

还是 facade 层的 setActive 方法:

最后在 reducer 里返回一个新的状态(有限状态自动机):

注意 callstack:

subscribeChanges

site context 发生变化之后,我们需要采用编程的方式,更新浏览器地址栏里的 url.

例子:当我从 language 下拉菜单里将语言从英语更改到中文时,断点立即触发:

此时 router url 里已经出现了 zh,这个值是什么时候写入的?

这行代码 ​​this.location.replaceState(serialized);​​ 单步执行之后,地址栏立即出现了zh:

语言发生变化之后,如何同步到浏览器地址栏的 url 字段?

切换成中文后,this.router.url 片段里,已经出现了 ​​zh​​,这是一个 get 实现:

调试器显示,这个 get 的执行根本还没结束:

上图是 Angular 框架代码,url 是通过 ​​this.serializeUrl(this.currentUrlTree)​​ 动态计算出来的。

router 的 urlSerializer 是我们动态注入进去的:

这里就开始执行我们自己的业务逻辑了:

params 参数为空:

segment,query 和 fragment 全部为空:

然后再从 urlEncodingParameters 里依次将 baseSite,language 和 currency 最新的参数值读取出来:

既然传进来的 params 里没有数据,就调用 SiteContextParamsService 读取参数值:

现在调用的就是 services 文件夹下的实现,之前已经了解过了:

从 service map 里读取数据:

所以 router 最后的值进行了更新:​​electronics-spa/zh/USD/​​

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:d匹配模板类型结构实例
下一篇:关于BufferedReader的read()和readLine()的区别
相关文章

 发表评论

暂时没有评论,来抢沙发吧~