react 前端框架如何驱动企业数字化转型与创新发展
667
2022-11-16
SAP Spartacus Site Context 实现专题页面
文章目录
site-context.module.ts
initSiteContextConfigforroot
config
config-loader/site-context-config-initializer.js
resolveConfiggetConfigisCurrentBaseSitegetUrlParams
site-context-config.ts (SiteContextConfig 类)context-config-utils.ts (两个工具函数)
getContextParameterValuesgetContextParameterDefault
default-site-context-config (defaultSiteContextConfigFactory)base-site-config-validator.ts
connectors
converters.tssite.adapter.ts ( SiteAdapter)site.connector.ts (SiteConnector)
events
site-context-event.module.tssite-context-event.builder.ts (SiteContextEventBuilder)
register 方法registerSetLanguage
facade (名称稍稍有点 confuse,里面的文件是 .service.ts)
site-context.interface.ts (SiteContext)base-site.service.ts (BaseSiteService)
StateWithSiteContextgetActivegetAll
currency.service.ts (CurrencyService)language.service.ts(LanguageService)
providers
context-ids.tscontext-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)
initializesetFallbackValuesetDefaultFromConfig
currency-state-persistence.service.ts (CurrencyStatePersistenceService)
initSynconRead
site-context-params.service.ts (SiteContextParamsService)
getContextParametersgetUrlEncodingParametersgetParamValuesgetSiteContextService
site-context-routes-handler.ts (SiteContextRoutesHandler)
initsetContextParamsFromRoutesubscribeChanges
Site Context 的实现位于 core\src\ 文件夹之下。
实现总共分下列八大部分:
每个子文件夹内都有一个 barrel file,index.ts.
site-context.module.ts
绝大多数 import 都是 site-context 文件夹自身的子文件夹提供的:
上图第一行,从 @angular/core 导入 ModuleWithProviders,这是一个包装器:
将 NgModule 与provider关联的 NgModule 的包装器。不推荐使用没有泛型类型的用法。 该 Module 对外只暴露了两个方法:
initSiteContextConfig
注入值:
SiteContextConfigInitializerSiteContextConfig
如果 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 能推测出,对于某个 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
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
这里的 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
上述代码,调用 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小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~