探索CHAOS:构建服务驱动UI的后端架构
CHAOS概述
CHAOS是Yelp使用的服务驱动UI框架。当客户端需要显示CHAOS驱动的内容时,它会向CHAOS API发送GraphQL查询。API处理查询,请求CHAOS后端构建配置,格式化响应,并将其返回给客户端进行渲染。
CHAOS API
CHAOS后端通过基于GraphQL的CHAOS API接受客户端请求。在Yelp,我们采用Apollo Federation作为GraphQL架构,利用Strawberry实现联邦Python子图,以利用类型安全的模式定义和Python的类型提示。CHAOS特定的GraphQL模式位于其自己的CHAOS子图中,由Python服务托管。
这种联邦架构使我们能够独立管理CHAOS特定的GraphQL模式,同时将其无缝集成到Yelp更广泛的超级图中。
在GraphQL层之后,我们支持多个实现CHAOS REST API的CHAOS后端,以CHAOS配置的形式提供CHAOS内容。这种架构允许不同团队在其自己的服务上独立管理其CHAOS内容,而GraphQL层为客户端请求提供统一接口。CHAOS API对请求进行身份验证并将其路由到相关的后端服务,其中处理大部分构建逻辑。
CHAOS后端
CHAOS后端的主要目标是构建CHAOS SDUI配置。此数据模型包含客户端配置CHAOS驱动的SDUI视图所需的所有信息。以下是一个名为"consumer.welcome"的CHAOS视图及其配置的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
{
"data": {
"chaosView": {
"views": [
{
"identifier": "consumer.welcome",
"layout": {
"__typename": "ChaosSingleColumn",
"rows": [
"welcome-to-yelp-header",
"welcome-to-yelp-illustration",
"find-local-businesses-button"
]
},
"__typename": "ChaosView"
}
],
"components": [
{
"__typename": "ChaosJsonComponent",
"identifier": "welcome-to-yelp-header",
"componentType": "chaos.text.v1",
"parameters": "{\"text\": \"Welcome to Yelp\", \"textStyle\": \"heading1-bold\", \"textAlignment\": \"center\"}}"
},
{
"__typename": "ChaosJsonComponent",
"identifier": "welcome-to-yelp-illustration",
"componentType": "chaos.illustration.v1",
"parameters": "{\"dimensions\": {\"width\": 375, \"height\": 300}, \"url\": \"https://media.yelp.com/welcome-to-yelp.svg\"}}"
},
{
"__typename": "ChaosJsonComponent",
"identifier": "find-local-businesses-button",
"componentType": "chaos.button.v1",
"parameters": "{\"text\": \"Find local businesses\", \"style\": \"primary\"}, \"onClick\": [\"open-search-url\"]}"
}
],
"actions": [
{
"__typename": "ChaosJsonAction",
"identifier": "open-search-url",
"actionType": "chaos.open-url.v1",
"parameters": "{\"url\": \"https://yelp.com/search\"}"
}
],
"initialViewId": "consumer.welcome",
"__typename": "ChaosConfiguration"
}
}
}
|
配置包括视图列表,每个视图都有唯一标识符和布局。如果有多个视图,initialViewId指定应首先显示哪个视图。布局(如此示例中的单列布局)根据其组件ID将组件组织到部分中,帮助客户端确定组件在CHAOS视图中的位置。
此外,配置列出了组件和操作,详细说明了它们各自的设置。每个组件可能有自己的操作,例如按钮的onClick操作。屏幕也可能具有在特定阶段触发的操作,例如onView,用于日志记录等目的。
CHAOS元素
在CHAOS中,组件和操作是基本构建块。我们没有在GraphQL层为每个元素定义单独的模式,而是使用JSON字符串表示元素内容。这种方法保持了稳定的GraphQL模式,并允许对新元素或版本进行快速迭代。
为确保正确配置,每个元素都定义为Python数据类,提供清晰的接口。类型提示指导开发人员了解预期参数。这些组件和操作可通过共享的CHAOS Python包获得。例如,文本组件可以结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@dataclass
class TextV1(_ComponentData):
value: str
style: TextStyle
color: Optional[Color] = None
textAlignment: Optional[TextAlignment] = None
margin: Optional[Margin] = None
onView: Optional[List[Action]] = None
onClick: Optional[List[Action]] = None
component_type: str = "chaos.component.text.v1"
text = Component(
component_data=TextV1(
value="Welcome to Yelp!",
style=TextStyleV1.HEADING_1_BOLD,
textAlignment=TextAlignment.CENTER,
)
)
|
这些数据类在内部处理Python数据类到JSON字符串的序列化,如下所示:
1
2
3
4
|
{
"component_type": "chaos.text.v1",
"parameters": "{\"text\": \"Welcome to Yelp\", \"textStyle\": \"heading1-bold\", \"textAlignment\": \"center\"}}"
}
|
这些基本组件和操作与容器类组件(如垂直和水平堆栈)结合使用时,能够实现强大的UI构建能力。
构建配置
在本节中,我们将探讨CHAOS如何构建配置。尽管过程可能很复杂,但共享的CHAOS Python包(也包含CHAOS元素)提供了在后台管理大部分构建过程的Python类。这使得使用CHAOS SDUI框架的后端开发人员可以专注于配置其内容。以下是构建过程的高级概述,后续部分将详细检查每个步骤。
步骤1:请求
当客户端向CHAOS API发送GraphQL查询时,它提供视图名称和上下文。视图名称用于将请求路由到相关的CHAOS后端以构建配置。上下文是一个JSON对象,被转发到后端,包括客户端规范或功能规范等信息。这允许后端为每个客户端请求自定义构建。
为了说明此过程,以下是来自移动设备的简化请求,演示如何检索名为"consumer.welcome"的CHAOS视图:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
POST /graphql
Request Body:
{
"query": "
query GetChaosConfiguration($name: String!, $context: ContextInput!) {
chaosConfiguration(name: $viewName, context: $context) {
# The actual fields of ChaosConfiguration would be specified here
...ChaosConfiguration Schema...
}
}
",
"variables": {
"viewName": "consumer.welcome",
"context": "{\"screen_scale\": \"foo\", \"platform\": \"bar\", \"app_version\": \"baz\"}"
}
}
|
收到请求后,CHAOS子图将其路由到CHAOS后端服务进行进一步处理。
步骤2:视图选择
单个CHAOS后端可以支持各种CHAOS视图。ChaosConfigBuilder允许后端开发人员注册其ViewBuilder类,这些类管理单个视图构建。收到请求后,ChaosConfigBuilder中封装的逻辑根据请求的视图名称选择相关的ViewBuilder并执行视图构建步骤,构建最终配置。以下是实践中使用ChaosConfigBuilder的简化示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from chaos.builders import ChaosConfigBuilder
from chaos.utils import get_chaos_context
from .views.welcome_view import ConsumerWelcomeViewBuilder
def handle_chaos_request(request):
# Obtain the context for the CHAOS request
context = get_chaos_context(request)
# Register the view builders supported by this service.
ChaosConfigBuilder.register_view_builders([
ConsumerWelcomeViewBuilder,
# Add other view builders here
])
# Build and return the final configuration
return ChaosConfigBuilder(context).build()
|
步骤3:布局选择
每个视图都有一个ViewBuilder类,它选择适当的布局并管理视图的构建。
CHAOS支持不同的布局。例如,前面示例中显示的单列布局只有一个"main"部分。其他布局,如基本移动布局,包括其他部分,如工具栏和页脚。这种灵活性允许内容在不同的客户端(如Web和移动设备)上以不同方式呈现,以适应不同的客户端特性。
CHAOS中每个支持的布局类型都有相应的LayoutBuilder。该类接受每个部分的FeatureProvider类列表(稍后详细描述)。每个部分中FeatureProviders的顺序决定了它们在客户端上呈现时的顺序。
继续使用welcome_consumer示例,ViewBuilder如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from chaos.builders import ViewBuilderBase, LayoutBuilderBase, SingleColumnLayoutBuilder
from .features import WelcomeFeatureProvider
class ConsumerWelcomeViewBuilder(ViewBuilderBase):
@classmethod
def view_id(cls) -> str:
return "consumer.welcome"
def subsequent_views(self) -> List[Type[ViewBuilderBase]]:
"refer to 'Advanced Features - View Flows' section for details"
return []
def _get_layout_builder(self) -> LayoutBuilderBase:
"""
Logic to select the appropriate layout builder based on the context.
"""
return SingleColumnLayoutBuilder(
main=[
WelcomeFeatureProvider,
],
context=self._context
)
|
当ChaosConfigBuilder执行ViewBuilder的构建步骤时,它在内部调用_get_layout_builder()方法来确定适当的LayoutBuilder并执行其构建步骤。在此示例中,该方法返回一个SingleColumnLayoutBuilder,该构建器结构化为单个名为"main"的部分。此部分仅包含一个功能提供者:WelcomeFeatureProvider。然后,LayoutBuilder将执行FeatureProvider的构建过程,该过程构建功能的SDUI配置。
步骤4:构建功能
功能的SDUI包括一个或多个组件和操作,它们共同实现产品目的,允许用户在Yelp应用上查看和与之交互。功能开发人员通过继承FeatureProvider类来定义每个功能,该类封装了加载功能数据和适当配置用户界面所需的所有逻辑。
每个FeatureProvider通过以下主要步骤构建其功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class FeatureProviderBase:
def __init__(self, context: Context):
self.context = context
@property
def registers(self) -> List[Register]:
"""Sets platform conditions and presenter handler."""
def is_qualified_to_load(self) -> bool:
"""Checks if data loading is allowed."""
return True
def load_data(self) -> None:
"""Initiates asynchronous data loading."""
def resolve(self) -> None:
"""Processes data for SDUI component configuration."""
def is_qualified_to_present(self) -> bool:
"""Checks if configuration of the feature is allowed."""
return True
def result_presenter(self) -> List[Component]:
"""Defines component configurations."""
|
一个视图可以包含多个功能,在构建过程中,所有功能都是并行构建的,以提高性能。为了实现这一点,功能提供者被迭代两次。在第一个循环中,启动构建过程,触发对外部服务的任何异步调用。这包括步骤:registers、is_qualified_to_load和load_data。第二个循环等待响应并完成构建过程,包括步骤:resolve、is_qualified_to_present和result_presenter。(值得一提的是,最新的CHAOS后端框架引入了使用Python asyncio的下一代构建器,这简化了接口。这将在未来的博客文章中探讨。)
检查注册
CHAOS中的Register类对于确保返回给客户端的任何SDUI内容都受支持至关重要。每个注册指定:
- 平台:注册配置所针对的平台(例如iOS、Android、Web)
- 元素:此配置中客户端必须支持的必需组件和操作
- 呈现器处理程序:如果满足所有条件,负责构建配置的关联处理程序(例如result_presenter)
在设置期间,开发人员可以定义多个注册,每个注册链接到不同的处理程序。根据提供给后端的客户端信息,选择第一个合格注册的呈现器处理程序来构建配置。如果没有注册合格,则该功能将从最终响应中省略。
检查加载资格
资格步骤is_qualified_to_load允许开发人员执行额外检查,以决定是否应继续功能构建过程以及是否应加载功能数据。这通常是应用功能切换或进行实验性检查的地方。如果此步骤返回false,则该功能将从最终配置中排除。
异步数据加载和解析
在load_data阶段,我们并行启动对上游服务的异步请求。我们将解析和阻止结果推迟到resolve阶段。这种方法能够在所有功能提供者中高效地分发请求和共享数据,通过在稍后阶段解析数据来优化性能。
检查呈现资格
资格步骤is_qualified_to_present允许开发人员执行额外检查,以确定是否应将功能包含在配置中。当需要加载步骤期间获取的数据来决定是否应显示该功能时,这尤其有用。如果返回false,则该功能将从最终配置中删除。
配置功能
这是我们配置构成功能的组件和操作的阶段。在FeatureProvider代码中,这由result_presenter方法表示。开发人员可以定义多个呈现器处理程序。在注册中选择的那个将作为功能的最终处理程序。
回到示例,当满足以下条件时,WelcomeFeatureProvider功能会显示给用户:请求客户端在iOS或Android平台上,并且客户端支持所需的CHAOS元素(TextV1、IllustrationV1、ButtonV1)。如果满足条件,则异步请求在load_data方法中获取按钮文本,然后在resolve方法中处理。result_presenter方法配置并显示欢迎文本、插图和带有获取文本的按钮。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
class WelcomeFeatureProvider(ProviderBase):
@property
def registers(self) -> List[Register]:
return [
Register(
condition=Condition(
platform=[Platform.IOS, Platform.ANDROID],
library=[TextV1, IllustrationV1, ButtonV1],
),
presenter_handler=self.result_presenter,
)
]
def is_qualified_to_load(self) -> bool:
return True
def load_data(self) -> None:
self._button_text_future = AsyncButtonTextRequest()
def resolve(self) -> None:
button_text_results = self._button_text_future.result()
self._button_text = button_text_results.text
def result_presenter(self) -> List[Component]:
return [
Component(
component_data=TextV1(
text="Welcome to Yelp!",
style=TextStyleV1.HEADER_1,
text_alignment=TextAlignment.CENTER,
)
),
Component(
component_data=IllustrationV1(
dimensions=Dimensions(width=375, height=300),
url="https://media.yelp.com/welcome-to-yelp.svg",
),
),
Component(
component_data=ButtonV1(
text=self._button_text,
button_type=ButtonType.PRIMARY,
onClick=[
Action(
action_data=OpenUrlV1(
url="https://yelp.com/search"
)
),
],
)
)
]
|
错误处理
在具有多个功能的SDUI视图中,错误处理至关重要。在数据密集型后端中,上游请求可能会失败,或者可能发生意外问题。为了防止由于单个功能的问题导致完整的CHAOS配置失败,每个FeatureProvider在CHAOS构建过程中都包装在错误处理包装器中。如果发生异常,则删除单个功能,而视图的其余部分不受影响。除非开发人员选择将功能标记为"essential",这意味着其失败将影响整个视图。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
def error_decorator(f: F) -> F:
@wraps(f)
def wrapper(self, *args, **kwargs):
try:
return f(self, *args, **kwargs)
except Exception as e:
if self._is_essential_provider:
raise
log_error(exception=e, context=self._context)
return []
return cast(F, wrapper)
class ErrorHandlingExecutionContext:
def __init__(self, wrapped_element: ProviderBase) -> None:
self._wrapped_element: ProviderBase = wrapped_element
self._context: Context = self._wrapped_element.context
self._is_essential_provider: bool = self._wrapped_element.IS_ESSENTIAL_PROVIDER
# Other methods are omitted for brevity.
@error_decorator
def final_result_presenter(self) -> List:
...
|
当发生错误时,我们记录详细信息,例如功能名称、所有权信息、异常细节和额外的请求上下文。此日志记录有助于监控问题、生成警报以及在问题达到指定阈值时自动通知负责团队。
高级功能
上面的示例涵盖了一个相当基本的配置构建示例。现在,快速了解一些高级CHAOS功能。
视图流
在CHAOS配置模式中,“ChaosView - views"被定义为列表,初始视图由"ChaosView - initialViewId"指定。
CHAOS框架被设计为允许视图与多个"后续视图"链接。这些后续视图的配置也包含在"ChaosView - views"中,每个视图都有自己唯一的ViewId。
后续视图通过"CHAOS Action - Open Subsequent View"访问。此操作允许使用其关联的ViewId导航到另一个视图。此操作可以附加到组件的onClick事件,例如按钮,从而允许用户无缝导航。
1
2
3
4
5
6
7
|
@dataclass
class OpenSubsequentView(_ActionData):
"""`"""
viewId: str
"""The name of subsequent view this action should open."""
action_type: str = field(init=False, default="chaos.open-subsequent-view.v1", metadata=excluded_from_encoding)
|
构建后续视图的过程与主视图构建器的过程相同。要将视图构建器注册为主视图的后续视图,ViewBuilder类提供subsequent_views方法。
1
2
3
4
|
def subsequent_views(self) -> List[Type[ViewBuilderBase]]:
return [
# Add View Builders for Subsequent Views here.
]
|
此列表中的每个视图构建器都与主视图构建器一起构建,并存储在最终配置中的"ChaosView - views"列表中。这种设计允许开发人员定义称为"流"的视图序列,这些视图使用"OpenSubsequentView"操作互连。这种方法在用户需要快速浏览一系列密切相关内容的场景中特别有益。通过预加载这些视图,我们消除了对每个视图配置的额外网络请求的需要,从而通过减少延迟来增强用户体验。
以下是我们Yelp for Business移动应用中使用的CHAOS流示例,专门设计用于支持客户支持FAQ菜单。
视图占位符
在CHAOS中,我们允许CHAOS视图嵌套在另一个CHAOS视图中,客户端在显示父视图后加载该视图。这是使用称为视图占位符的特殊CHAOS组件实现的。在呈现此组件时,父视图最初默认显示加载微调器,直到嵌套视图的CHAOS配置成功异步加载。加载后,嵌套视图将与父视图的周围内容无缝集成。
这种方法使主要内容能够更快地显示给用户,而其他内容则在用户与屏幕上的其他项目交互时在后台加载。
视图占位符组件还可以选择配置为处理加载过程中的不同状态,包括加载、错误和空状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@dataclass
class ViewPlaceholderV1(_ComponentData):
"""
Used to provide a placeholder that clients should use to fetch the indicated CHAOS Configuration and then load the retrieved content in the location of this component.
"""
viewName: str
"""The name of the CHAOS view to fetch, e.g. "consumer.inject_me"."""
featureContext: Optional[ChaosJsonContextData]
"""
A feature-specific JSON object to be passed to the backend for the view building process by view placeholder.
"""
loadingComponentId: Optional[ComponentId]
"""An optional component that provides a custom loading state."""
errorComponentId: Optional[ComponentId]
"""An optional component that provides a custom error state."""
emptyComponentId: Optional[ComponentId]
"""An optional component that provides a custom empty state."""
headerComponentId: Optional[ComponentId]
"""An optional component that provides a static header."""
footerComponentId: Optional[ComponentId]
"""
An optional component that provides a static footer.
Use the footer to provide a separator between the component and content below it.
If the view is closed, the separator will be removed along with the view content.
"""
estimatedContentHeight: Optional[int]
"""An optional estimate for the height of the content so that space can be allocated when loading."""
defaultLoadingComponentPadding: Optional[Padding]
"""Specifies whether padding should be added around the shimmer."""
component_type: str = field(init=False, default="chaos.component.view-placeholder.v1")
|
以下是视图占位符在我们Yelp for Business主屏幕上的实际示例。完整的主屏幕由CHAOS支持。“Reminders"功能是另一个独立的CHAOS视图,由不同的CHAOS后端服务支持。ViewPlaceholder用于在主屏幕加载后异步获取Reminders,并将其定位在适当的位置。
更多CHAOS?
本文提供了CHAOS后端构建过程如何协同工作的高级概述。我们介绍了配置如何构建、功能如何组合和验证,以及视图流和嵌套视图等高级功能如何帮助创建动态、响应的用户体验。
在即将发布的帖子中,我们的客户端工程团队将更深入地探讨CHAOS如何在Web、iOS和Android上实现,以及每个平台如何适应服务驱动的配置,为用户提供无缝体验。我们还将探讨更高级的主题,例如使CHAOS更加动态的策略、优化性能以及扩展框架以支持日益复杂的产品需求。
我们很高兴继续分享我们在发展CHAOS以在Yelp上支持更丰富、更快、更灵活的用户体验方面所学到的东西。敬请期待!