作者。 邪恶火星人高级移动软件工程师Alexander Madyankin
看看我们是如何从Apollo驱动的设计中跳脱出来,为一个GraphQL支持的多平台前端应用设计,并采用了一个强大的、可扩展的方法,其灵感来自Alistair Cockburn的Hexagonal架构,我们将其应用于现代React、React Native和TypeScript栈。如果你从图表中获得灵感,并且喜欢解决高层次的设计难题,那么这篇文章就是为你准备的
这篇文章总结了我们在帮助一家美国房地产初创公司简化住房和创造更好的共同生活体验方面的经验。在这次合作中,我们建立了两个React Native应用程序,并得出了一个通用的方法来建立一个可扩展的,强大的,最重要的是,跨平台的应用程序,我们将在这篇文章中分享。
应用程序的目标是允许会员从他们的设备上访问coliving社区,通过聊天联系,保持所有公告的最新信息,获得独家福利和折扣,并参加和创建活动。最初的计划是在网络上迅速推出最小可行产品,以获得第一批用户,然后为iOS和Android建立移动客户端,使其具有相同的功能。
在Evil Martians,我们喜欢迭代工作,以便更快地响应不断变化的需求,因为变化是创业游戏中不可避免的一部分。以下是我们最终做的事情。
- 用React、Apollo、apollo-link-state(现在是apollo-link的一部分),在没有状态管理库的情况下,在JavaScript中快速发布一个网络MVP。
- 回到绘图板,从MVP中吸取经验,开发一个跨平台的应用程序,其中网络功能将通过React Native在iOS和Android上实现。
- 在第一个产品发布后,项目的范围不断扩大,并建立了第二个应用程序,其功能基本相似,但需要一个单独的项目从头开始。我们又一次去画板上画,最终达成了一个清晰的、可扩展的架构,可以长期服务于一个产品开发团队,并通过尽量避免代码重复来实现在多个平台(web、iOS、Android)上的发货功能。
我们将不多谈MVP,而专注于第二和第三阶段,因为我们要尝试两种不同的架构并对它们进行比较。本文将对这两种架构进行描述。
第一阶段。Apollo驱动的架构
Apollo是目前每个希望建立GraphQL支持的单页应用程序的前端开发人员最明显的选择。它是一个强大的工具,允许你建立一个基于GraphQL的API,然后用它来建立一个基于GraphQL的客户端。在搭建MVP时,Apollo是打基础的完美工具。它有一个强大的追随者和广泛的生态系统。
我们在产品的MVP阶段用Apollo有了一个良好的开端,所以我们决定把它带入 "最终 "实施阶段。很快我们了解到它有一些缺点,随着产品的成熟,这些缺点才显现出来,但后面会有更多的介绍。
我们还决定使用TypeScript,而不是普通的JavaScript,以便在代码进入运行时之前尽可能多地捕捉错误。另一个决定是放弃apollo-link-state ,因为我们了解到它与单一责任和依赖倒置原则不相称,并鼓励不受欢迎的代码耦合(在我看来,在API客户端缓存中保持状态不是最好的主意)。我们决定使用Redux进行状态管理,因为它可以将事情干净地分开。以下是初始架构的地图。
Apollo驱动的架构
核心负责配置、连接服务器、在屏幕间导航、调度事件、管理全局状态,这些都可以在应用的任何部分使用。
功能是应用程序的孤立部分,每个功能都提供单一的功能,对其他功能一无所知,并通过事件与它们进行通信。一个功能可以包含特定功能的GraphQL查询和突变的定义,与该功能相关的Redux状态的一部分,哑巴React组件,以及连接到状态和API的容器。
在这个设计中,来自GraphQL后端的数据绕过Redux状态,直接到达Apollo HOC(高阶组件),就像这样。
interface IProps {/* skipped */}
type TAllProps = ChildProps<IProps & IPostsQueryProps>;
class PostsList extends Component<TAllProps, IState> {
public render() {
const { postsData } = this.props;
if (postsData.isLoading) return <ScreenLoader />;
return (
<FlatList
data={postsData.posts}
renderItem={this.renderPost}
refreshing={this.state.isRefreshing}
onEndReached={postsData.fetchMore}
{/* skipped */}
/>
);
}
// skipped
}
export default compose(withPostsQuery())(PostsList);
流程管理器是用redux-saga实现的,管理不同的流程,如用户认证、收集分析、屏幕之间的导航、推送通知等。
库目录包含一些辅助功能和围绕React Native相关库的包装。
乍一看,所描述的方法似乎很合理,也很有效。我们在不同的应用功能之间有松散的耦合,在它们内部有高度的内聚力。这让我们可以在一个功能中改变一些东西,而不用担心我们会破坏一些东西。
然而,在进一步的开发中,出现了一些困难。
阿波罗任务的问题
首先,阿波罗和它的内部缓存出现了一堆的问题。每当我们遇到阿波罗的错误,我们不得不花一些时间来寻找其原因,并面临着一个昂贵的两难选择:挖掘库,提出拉动请求和交叉手指,或者找到一个解决的办法。
例如,我曾浪费了近一周的时间来修复一个无限滚动的bug,当一个新的数据部分反复出现和消失的时候。你可以看一下它在iOS模拟器中的样子。
阿波罗问题
阿波罗问题
我以为这个问题是由React Native的FlatList 或类似的东西引起的。其他火星人和我花了一些时间才意识到,问题出在Apollo代码中的请求和响应的顺序。也许,现在已经修好了,但它确实花费了我们宝贵的时间。
因为我们选择了基于组件的方法来与服务器通信,所以我们不能只是快速地把阿波罗换成另一个库。
为每个功能隔离子状态使我们在必要时很难在功能之间共享状态。我们没有地方可以放置这样的状态以使其干燥,所以我们需要通过事件和进程管理器进行同步。这也造成了一些错误。
虽然移动应用是网络应用的克隆,但它仍然是一个独立的代码库,所以我们仍然不得不复制和粘贴代码来重用它,在这个过程中为React Native做调整。
换句话说,我们的应用功能之间有松散的耦合,但API和UI层之间有强耦合。
我们有几乎相同的API查询与React和React Native组件强耦合,以及不同平台的状态管理方法。当我们需要把一些东西从网络应用移植到移动应用时,我们不得不重新编写所有的UI组件,并重写状态管理代码使其工作--这不是最有效的方法。
除了这些问题,我们还遇到了一些性能方面的问题,因为嵌套的Apollo高阶组件,不可预测的重新渲染,以及由于使用了样式化组件--有时它们会使每个React组件的渲染时间增加2-5倍。所以我们也花了一些时间来修复这些问题。
当然,当我们最终修复了所有的bug并发布了第一个版本的生产应用时,产品需求也发生了变化:现在我们必须在短短的六周内为iOS和Android提供一个更先进的跨平台应用。
第二步:建立一个六边形
在Apollo的成功之后,考虑到时间的紧迫性和范围的扩大,我们做出了一个大胆的决定,再次从头开始,这一次我们从一开始就设定了明确的目标。
- 使重构应用程序和替换其中任何部分变得容易。
- 提高可维护性,降低产生错误和累积技术债务的概率。
- 尽可能使代码在不同的平台之间可重复使用。
- 使得应用程序快速而流畅。
- 要使离线模式的实现变得容易。
如果我们把应用程序看作是一个工具箱,那么架构就是所有的抽屉、盒子和装好东西的笔的布局。为了达成一个好的架构,我们需要把重点放在组织东西和设置不同工具之间的界限上,这样它们就不会被混淆。所以现在是时候忘记库本身了。React、Redux、Apollo、MobX,你的名字,只是构建模块。
我们的目标是建立一个应用程序,在那里这些块可以单独构建、替换、重构和调试。这样的方法减少了开发和测试时间,给我们提供了高可维护性,并减少了技术债务。增加新的代码不需要大规模的修改,调试时需要尽可能少的变通,测试也不那么麻烦。
由于我有充分的授权在高层次上工作,并且有火星人团队的力量在背后支持我建立一个实际的实现,我决定尝试我考虑了很久的东西:实现Alistair Cockburn的Hexagonal架构,他是《敏捷软件开发宣言》背后的伟大人物之一。
从我所有的经验来看,它似乎是SPA和React Native应用的合理选择,而且它正是我想要的稳健架构。我所要做的就是对它进行一些调整。
根据Cockburn的说法,六边形架构由多个层次组成。每层只在自己和外部层之间有一个边界,并且与其他层解耦,对其他层一无所知,但定义了一些契约。
我决定将应用程序分成核心层、数据提供者、应用程序和框架层。
在我们的例子中,合约是TypeScript接口,定义了与相关层交互的可能方式。我们把这样的接口放到每个层内的interfaces.ts 文件中。经验法则是,当我们需要对一个概念的多个实现时,就使用接口。另外,当我们想从低级别的实现中抽象出来时,定义一个接口也是一个好主意,可以避免各层之间的混淆。
一个新闻数据提供者的合同的例子可能是这样的。
interface INewsDataProvider {
fetchPost: (id: string) => Promise<FetchPostResult>;
fetchPosts: (limit?: number) => Promise<FetchPostsResult>;
}
为了传输数据并保持各层和代码的其他部分不耦合,我们定义并使用DTOs(数据传输对象)。我们也将它们定义为TypeScript接口。
六边形内部的运动是从外面的一层开始的:我们从一个环绕的层中向每一层注入依赖关系。这些依赖必须实现内层定义的相应接口。
为我们的应用程序中可能发生变化的部分使用接口是一种隔离变化的方法。通过接口,我们可以创建新的实现,或者根据需要在现有的实现周围添加更多的功能,而不必担心破坏应用程序。
接下来,我将浏览所有的层,描述它们的目的,然后向你展示我们是如何实现它们的。
核心层
这一层是应用程序的本质--它包含了业务逻辑,保留了我们需要的所有数据,并定义了外部层如何与之交互。核心层必须对UI或API的实现一无所知。它不关心它的依赖是否在最近的外层或任何更远的层中实现。
核心唯一关心的是,该依赖是否实现了核心定义的接口。这样的方法允许我们心无旁骛地测试和编写代码。
我们甚至可以将核心提取为一个独立的库,以便随时在另一个平台上重用它。
核心层
核心层的隔离也使奇妙的事情成为可能,就像《可重用的javascript状态管理的奇特案例》中所描述的那样。
"有些情况下,我们有一个非常大的初始状态,可以在nodejs Lambda中对我们的状态进行水化,并将调和后的状态发送给UI,所以我们在浏览器上的计算量非常小。这是唯一可能的,因为我们的状态是一个独立的包,可以被导入到任何javascript环境中"。
为了进行计算和改变状态,我们在核心部分定义了命令,并将它们输出到外层使用。每个命令在业务逻辑上都是一个 "动作"。这类命令的例子有:"更新用户的名字","获得最新的新闻","选择要显示的资料"。
当不清楚一些数据和代码应该进入核心层还是应用的用户界面时,理解业务逻辑和让用户与逻辑互动的图形界面之间的区别是很有用的。
假设我们不是有一个而是有两个应用程序:一个是移动应用程序,一个是具有相同的核心逻辑但可能有不同的UI实现的Web应用程序,这可能会有帮助。
Michel Weststrate,MobX的作者,在他的文章"作为事后考虑的UI "中提到了如何决定什么应该进入核心逻辑,顺便说一下,这是一篇很好的文章。
"最初,设计你的状态、存储、流程,就像你在构建一个CLI,而不是一个Web应用......没有什么比直接将你的业务流程作为一组函数来调用更简单了。
数据提供者层
这一层的本质是成为核心层和外层之间的调解人。
我们不从应用层和框架层导入任何代码到这一层,也不在这里放置任何低级别的实现。然而,我们可以从核心层导入代码。
数据提供方层
数据提供者层包含了核心层中定义的接口的实现。我们使用这些实现来向API客户端和持久化存储提出请求,并向核心层提供数据。
除了核心接口的实现之外,该层还可以包含我们应该在应用层或框架层实现的IApiClient 和IPersistedStorage 接口的定义。我们需要它们来向外部世界发出请求,而不知道接口的低级实现。
此外,我们可以在这一层放置模式验证和数据转换器,以验证来自服务器的数据并避免运行时的错误。
和核心层一样,数据提供者层可以重复使用,以实现一个网络应用,而不需要改变。在这种情况下,我们可能想把它提取到一个单独的包中。
应用层
应用层包含应用程序本身的实现。在这里,我们放置导航、UI、推送通知管理器、管理应用程序生命周期的Application 单元,以及将所有部分粘合在一起的代码。
我们可以从核心层、数据提供者和框架层导入代码到这一层。
应用层
在Application 单元中,我们实例化数据提供者、存储、API客户端和其他东西,并将它们注入内部应用层:核心层和数据提供者层。
UI被分割成功能,功能本身由屏幕组成。通常,一个功能包含两个目录--screens 和components 。功能的components 文件夹只包含与功能相关的组件。
我们尽可能地保持UI的单薄,将所有的业务逻辑和数据移到核心部分。然而,我们可以使用UI组件的内部状态来保持其数据,如果它在组件之外没有意义的话。
框架层
这一层的目的是保持我们的应用程序所使用的各种库的适配器。这种适配器的例子是错误报告器、文件上传器、持久化存储实现。你不应该在应用程序中直接使用一个库。编写适配器,以使你在必要时可以轻松地抛弃一个库。
框架层
当实现被封装并遵循定义好的接口时,添加、修改或替换库和扩展框架就变得更加容易。
我们可以把一些实用程序放到这一层(例如,数组或动画助手),而不需要实现一个接口。我们必须确保只将它们导入应用层,而不在内层中使用它们。如果我们需要将某些东西导入内层,我们必须定义相应的接口并在框架层实现它。
错误处理
如果没有一个定义明确的错误处理流程,任何架构都不可能是健壮的。我们做了一些尝试来思考这个问题,并在实现我们想要的流程之前做了一些实验。
我们只允许在低级别的实现中出现异常:网络请求失败、本地代码或第三方库中的意外错误等等。此外,当服务器返回200 OK,但响应体包含一个意外的错误时,也会出现隐含的异常。
每次异常发生时,我们必须处理它,并将其转换为核心层中定义的相应错误类型。
为了定义一个操作是否成功,使用结果类型是很方便的。我们在核心层中与错误类型一起定义这种类型,并在数据提供者和应用层中使用它们。
下面是这个流程的样子。

错误处理流程
如果一个错误发生在应用层,我们不把它保存到核心层,而是在UI中处理它,以保持事情的简单性。这通常是当我们使用框架中的一个库,并且它抛出一个异常或调用一个错误回调时的情况。
实现
一旦我们定义了架构,我们就必须选择工具来实现它并构建应用程序。
每个应用程序的第一个和主要的工具是用什么语言来编写应用程序。我们选择了TypeScript,因为它是静态类型的,有接口,而且我们希望在开发时尽可能多地捕捉到bug。
为了实现核心部分,我们选择了Redux。UI是用React实现的,应用程序是用React Native编写的。然而,我们的架构允许我们在必要时交换库而不触及任何其他代码。
核心的结构
我们在这里有一些重要的部分:状态、事件分配器、层的公共接口定义、连接核心和React的适配器(如果有必要,我们可以添加其他适配器),以及初始化核心和直接与状态互动的Core 对象。
为了使Redux动作和还原器的类型安全,避免愚蠢的错误,我们写了一些辅助工具来推断类型。类型定义增加了一些模板,但我们用Hygen的代码生成器解决了这个问题。
状态被划分为子状态。子状态之间必须互不相识。
下面是state 目录的模样。
├── state
│ ├── actions.ts # actions object to use in the `Core` object
│ ├── common # models and interfaces to use in multiple sub-states
│ │ ├── index.ts
│ │ ├── interfaces
│ │ ├── models
│ │ └── samples.ts
│ ├── aTypicalSubstate # sub-state
│ │ ├── __tests__ # unit tests
│ │ │ ├── actions.ts
│ │ │ └── state.ts
│ │ ├── actions.ts # Redux actions and types
│ │ ├── errors.ts # error definitions
│ │ ├── helpers.ts # optional helpers module
│ │ ├── index.ts # umbrella module to export public definitions
│ │ ├── interfaces.ts # public interfaces
│ │ ├── state.ts # the sub-state itself
│ │ └── validators.ts # optional module with validators
│ ├── global.ts # the `IGlobalState` interface definition to use in sub-states
│ ├── rootReducer.ts # all the sub-states collected in a single object
│ └── selectors.ts # global selectors which use data from several sub-states
行动
为了类型安全起见,我们将我们的动作定义为类。为了使其与Redux正常工作,我们在中间件中将其转换为普通对象。
export enum Types {
RESET = "CURRENT_USER/RESET",
SET = "CURRENT_USER/SET",
}
class ResetAction implements IAction {
public readonly type = Types.RESET;
}
class SetCurrentUserAction implements IAction {
public readonly type = Types.SET;
public constructor(public data?: ICurrentUserData) {}
}
一旦我们定义了动作,我们就可以在Actions 地图中使用它们来推断类型,并将动作创建者与dispatch 函数绑定--只是通常的Redux东西与相应类型。
每个异步动作都会返回一个包含结果的承诺。在这一点上,所有可能出现的异常都已经被相应的数据提供者处理了。因此,我们可以采取的结果,解包,调度行动,并返回结果,以便以后在UI中使用。
下面是Actions 对象的样子。
export const Actions = {
reset: () => new ResetAction(), // Action creator for ResetAction
// An async action
fetchCurrentUserData:
() =>
async (dispatch, _, { currentUser }) => {
// Here we use the injected data provider implementation
// for the `ICurrentUserDataProvider` interface
const result = await currentUser.fetchCurrentUser();
// The data provider returns the `Result` type
// which we have to unwrap first to get the data
if (result.is_ok()) {
const data = result.ok();
dispatch(new SetCurrentUserAction(data));
}
// We return the result to use it in UI components to see if there is an error
return result;
},
};
// `ActionTypes` is a union type with all the actions.
// We use it in a reducer to infer actions types
export type ActionTypes = ActionsUnion<ActionCreatorsMap>;
// `ICurrentUserActions` is the final action creator's map type without the unnecessary information about internals which we export from the core and use in the outer layers.
export type ICurrentUserActions = MakeBoundActionCreatorsMap<ActionCreatorsMap>;
// The namespaced actions type to use in exports
export interface INamespacedCurrentUserActions {
currentUser: ICurrentUserActions;
}
// Create actions and export them to use outside
export const mapDispatch = (
dispatch: Dispatch
): INamespacedCurrentUserActions => ({
currentUser: bindActionCreators<typeof Actions, ICurrentUserActions>(
Actions,
dispatch
),
});
状态
当动作被定义后,我们可以实现状态和更新函数(Redux中的一个reducer)。我们使用Immer来轻松地、不变地更新状态,使用Reselect来为存储和计算的数据创建备忘录化的选择器。
import { ActionTypes, Types } from "./actions";
export interface IState {
// The current user state definition
}
// The namespaced state definition to create the namespaced state object
// to merge with the global app state
export interface ICurrentUserState {
currentUser: IState;
}
export const initialState: IState = {
id: undefined,
//...
};
export const update = immer((draft: IState, action: ActionTypes) => {
switch (action.type) {
case Types.SET: {
// We pass the data using a DTO
const { data } = action;
// Thanks to Immer, we can just assign the data
if (data && data.user) {
draft.id = data.user.id;
draft.someData = data.user.someData;
}
return;
}
case Types.RESET: {
return initialState;
}
}
}, initialState);
const currentUserSelector = (state: { currentUser: IState }): IState =>
state.currentUser;
// A selector to select the stored and computed data
// To compute data, we user memoized `createSelector` from `reselect`
export const getStateWithDerivedData = createSelector(
currentUserSelector,
(currentUser): ICurrentUserState => ({
currentUser: {
...currentUser,
// Here we can place some computed data
},
})
);
React适配器
为了在React组件中使用所有的子状态,我们为React实现了一个适配器。它导入所有的子状态定义和它们的动作,将它们连接到Redux存储,并为每个子状态导出一个连接的高阶组件。
为了在用户界面之外使用状态和动作,我们在Core 对象上定义了actions getter,它返回一个带有每个子状态的命名动作的对象,以及store getter,它返回Redux商店对象。
通过React适配器和Core对象导出状态和动作
下面是我们如何实现React适配器的。我们为我们的每个子状态定义类型并建立相应的HOC,并将它们合并到全局Redux状态中。另外,我们在这里做了存储提供者组件,为UI组件提供我们的数据。
import * as currentUser from "./state/currentUser";
//... and other imports
// Namespaced state type definition.
// The state and its actions will be placed under the `currentUser` key in the global state
export type ICurrentUserProps = currentUser.ICurrentUserState &
currentUser.INamespacedCurrentUserActions;
// Connected Redux higher-order component
export const connectCurrentUserState = connect(
(state: IGlobalState) => ({
currentUser: {
...currentUser.getStateWithDerivedData(state).currentUser,
profile: currentUserProfileSelector(state),
},
}),
currentUser.mapDispatch,
// Merging our sub-state into the global state
(state, actions, props: object): ICurrentUserProps => ({
...props,
currentUser: { ...state.currentUser, ...actions.currentUser },
})
);
// ... Exports for the rest of the connected components
// Store provider to wrap the root UI component
export const StoreProvider = ({ children }) => (
<Provider store={getStore()}>{children}</Provider>
);
Core 对象的定义是非常简单的。它只是重新导出了商店和行动,并调用了一些内部的东西。另外,我们使用ICoreInitOptions 接口来定义所有的依赖关系和选项。
我们从Application singleton中调用Core.init 方法,并在启动应用程序时注入依赖关系。
interface ICoreInitOptions {
config: IStoreConfig;
dataProvider: IDataProvider;
dispatcher: EventDispatcher<CoreEvents>;
errorReporter: IErrorReporter;
afterInitCallback: () => unknown;
}
export const Core = {
init(options: ICoreInitOptions): void {
// We inject all the passed dependencies and configure the store here
},
get actions() {
// Return the namespaced actions of every sub-state
// to use outside React
},
get store() {
// Return the Redux store object to use outside React
},
resetState() {
// Reset all the sub-states
},
};
数据提供者的细节
我们有一个GraphQL后端,所以数据提供者层有查询和突变的定义。我们决定不再使用Apollo或任何其他第三方GraphQL客户端。相反,我们在Fetch API周围做了一个小的封装,把实现放在应用层的API客户端中,每次我们想从数据提供者中进行查询时都会使用它。
这一层是框架层之外唯一允许出现异常的地方。在这里,我们捕捉失败的网络请求,本地代码中的意外错误,以及所有可能的异常和服务器错误,并将它们打包到Result 。
数据提供者的概念使得在应用程序中实现具有可预测行为的离线模式变得容易。下面是我们如何实现一个数据提供者来获取新闻并回退到本地缓存。
// The `INewsDataProvider` is defined in the core
export class NewsDataProvider implements INewsDataProvider {
private static readonly storageKey = "news/posts";
// We inject our API and local storage clients here
public constructor(
private client: IApiClient,
private storage: IPersistedStorage
) {}
private readPostsFromCache(): Promise<IPost[] | undefined> {
return this.storage.getItem<IPost[]>(NewsDataProvider.storageKey);
}
private async writePostsToCache(posts: IPost[]): Promise<void> {
return this.storage.setItem(NewsDataProvider.storageKey, posts);
}
private async fetchPostsFromCache(limit: number): Promise<FetchPostsResult> {
const cache = await this.readPostsFromCache();
return cache
? Ok(cache.slice(0, limit))
: Err(new NewsError("transportError"));
}
private async fetchPostsFromAPI(limit: number): Promise<FetchPostsResult> {
try {
const response: IFetchPostsQueryResult = await this.client.query(
fetchPostsQuery,
{ limit }
);
return Ok(response.posts);
} catch (error) {
return Err(new NewsError("transportError"));
}
}
// First, we try to fetch posts from the server and save them to the cache
// Of something goes wrong, we try to fetch the posts from the storage
public async fetchPosts(limit = 20): Promise<FetchPostsResult> {
const result = await this.fetchPostsFromAPI(limit);
if (result.is_ok()) {
this.writePostsToCache(result.ok());
return result;
} else if (result.err().kind === "notFound") {
return result;
}
return this.fetchPostsFromCache(limit);
}
}
应用程序单子
我们使用React、React Navigation、React Native的一些库和框架层的包装器来实现应用层。
该层的核心是Application singleton。它启动应用程序,注入依赖关系,并将所有的部分粘合在一起。简而言之,它是这样定义的。
export class Application {
private client?: ApiClient;
private notificationsManager!: NotificationsManager;
public readonly config = new Config();
public readonly dispatcher = new EventDispatcher<AppEvents>();
public readonly storage!: IPersistedStorage;
public readonly errorReporter!: IErrorReporter;
public readonly dataProvider!: IDataProvider;
// Inject dependencies for the inner layers
public constructor() {}
// Inject dependencies for the inner layers, subscribe to core events
public init(): void {}
// Fetch the user data and navigate to the proper screen
public handleLogin = async () => {};
public get apiClient(): ApiClient {
return this.client || (this.client = this.buildApiClient());
}
private buildApiClient(): ApiClient {
const actions: IApiClientActions = {
onLogin: () => this.onLogin(), // E.g., store the access token
onLogout: () => this.onLogout(), // E.g., clean the user data and token
getUploader: (...args): IUploader => new AWSUploader(...args),
};
return new ApiClient(this.config, actions, this.storage);
}
}
关于框架层的更多信息
这一层包含了核心层、数据提供者和应用层中定义的接口的实现。当实现被封装并遵循定义的接口时,添加、修改或替换库和扩展框架就变得更加容易。
另外,我们在这里放了一些简单的实用工具,供应用层使用。
一旦我们启动了应用程序的架构,在短短一个月内,由一个五人团队构建应用程序本身是非常容易的。
通过六边形架构和TypeScript,我们得到了一个可扩展的、快速的、反应灵敏的应用,用户体验流畅,几乎没有运行时错误。现在,如果我们需要实现另一个应用程序,可以很容易地对应用程序进行修改,添加离线支持,编写测试,并重新使用业务和数据获取逻辑。
我们希望,一旦你在即将到来的React项目中需要解决与架构有关的挑战时,你能很好地利用这篇文章,尽管这种方法并不局限于React,或者,事实上,任何库,因为它允许你超越框架的限制,使你的项目永远更有可扩展性。
如果你需要招募火星人前端工程师来帮助你设计和构建你的下一个网络或移动应用程序,并在合作结束后易于维护,或者如果你的团队在建立未来的最佳实践方面需要一些帮助,请随时给我们打电话。