51 months ago

TypeScript SDK 和 REST API

在本文中,我们将讨论CUBA平台中已经存在很长时间的一个功能,但是很多人还不知道,这就是前端SDK生成器,并了解它如何与CUBA的REST API插件一起使用。

Java+JavaScript - 在 web 中喜结连理

仅仅只是大概八年之前,我们 Java 开发者将 JavaScript(JS) 作为“二等公民”语言用在 Web 应用程序中。那时,用 JS 的目的只是为了在网页中加入一些动效,网页也是使用 JSF、Struts、Tapestry 或 Thymeleaf 等框架在服务器端生成的。如今,我们见证了 JS 作为使用React、Vue 或 Angular 等框架进行客户端开发的第一语言,甚至连 Node.js 也出现在了服务端。

实际应用中,我们开发的 Web 应用程序可能在不同的层上使用不同的语言:JS 用于客户端UI,Java 用于业务逻辑处理,SQL 用于从数据库中获取数据,Python 用于数据分析,等等。然后再使用各种技术将所有这些语言组合到一个应用程序中。最常见的例子就是 REST API,基于平台无关的 HTTP 协议和简单的 JSON 格式,现在已经成为连接客户端 JS 和服务器端 Java 的默认方法。

但是,即使是最好的裁缝也不能无缝缝制衣裳。 定义 API 始终存在一个问题:要调用的方法是哪个,数据模型是什么,还有比如说我们应该将街道地址作为结构化对象还是作为字符串传递?

我们如何才能帮助我们的 JavaScript 同事更快地创建代码还能避免沟通的不畅?

Swagger 会是终极答案吗?

也许你要说“Swagger”,没错。 Swagger 实际上已经是设计、构建、记录和使用 REST API 的工业标准。也有许多代码生成器可帮助生成用于不同语言的客户端 SDK。

CUBA 框架就支持 Swagger,每个带有 REST API 扩展组件的应用程序都有一个 API 端点可以用来下载 .json 或 .yaml 格式的 Swagger 文档。您可以使用这些文件来生成 JS 客户端。

但是事实上,Swagger 只是一个 API 文档工具。那么前端开发人员希望在 API 中看到什么样的信息呢? “经典”的做法是:将业务功能映射为服务,并构建一套定义规范的 API。再将这套 API 公开为一组 REST 服务,最后添加 Swagger 文档,差不多就可以了。

如果真的是这样的话,为什么 GraphQL 会快速崛起并在前端开发人员中引起轰动呢? 我们注意到,GraphQL 的份额一直在不断增长。到底是怎么回事?事实证明,遇到经常会变动的用例,提供更“通用”的 API 对前端人员来说更加合适,因为可以避免创建很多小的 API。比如,对于 Web UI 中的购物车情景,一个物品的价格只需要第一次调用时获取,之后对于该物品只需要知道总数即可。另外,GraphQL 可以避免过度获取数据或者数据获取不足,还能一次调用多个 API 获得复杂的返回数据结构。

这样的话,似乎应用程序不止应该开放服务的 API,还应该提供一些通用 API。这种方式使得前端开发人员可以调用复杂的业务功能,但是也为他们提供一定程度的灵活性。因此,如果他们只是需要针对不同 UI 获取数据的不同展现形式,他们不会要求提供新的 API。

还有一个问题是 Swagger 或 GraphQL 或 OData 都无法解决的,那就是如果更改了某些内容,针对之前已经生成的客户端代码,如何做适配呢?直接一次性生成代码很容易,但是能支持业务的变化却是另外一回事了。比如,我们删除了实体的一个属性,怎么保证前端应用程序不会出问题?

所以,综合来看,为了加速前端开发并简化后端团队与前端团队之间的协作,我们需要:

  1. 定义特定业务的 API 和通用 API
  2. 根据后端数据模型和方法签名来生成前端代码
  3. 如需修改生成的代码,需要保证花费最小的人力并且尽量避免 bug

我们在 CUBA 的 REST API 扩展组件和前端 SDK 生成器中也面临着相同的所有挑战。

CUBA TypeScript SDK

在CUBA中,REST API 扩展组件提供以下功能:

  • 数据模型实体的 CRUD 操作
  • 执行预定义的 JPQL 查询
  • 执行服务方法
  • 获取元数据(实体,视图,枚举,数据类型)
  • 获取当前用户权限(访问实体、属性以及特定权限)
  • 获取当前的用户信息(名称,语言,时区等)
  • 处理文件

功能上说,我们提供了从任何前端客户端访问应用程序的所有功能。这些 API 都通过swagger YAML 文件或者 JSON 文件进行说明,对于前端开发来说,可以使用这些文档立即进行开发。

为 REST API 用户设置安全规则也很重要,需要防止 API 端点意外暴露给所有用户。首先要禁止所有用户对于常规 REST API 访问,然后为需要访问某些功能的角色创建特殊的权限。

CUBA 不止能提供 REST API,还可以生成前端使用的 SDK,该 SDK 可以作为任何前端开发框架的基础,React、Angular、Vue 或者其他框架都能用。

用这个生成器能生成一组 TypeScript 类,通过这些类可以在客户端应用程序调用CUBA 的 API。

如果要生成 SDK,可以运行下列命令:

npm install -g @cuba-platform/front-generator

然后:

gen-cuba-front sdk:all

该功能会创建所有的 SDK 类。甚至能基于 ReactJS 直接生成一个简单的 UI,这样客户能立即使用基于 CUBA 的应用程序。虽然生成的 UI 非常基础,但是由于集成了 CUBA,所有 CUBA 的功能都可以使用,包括身份验证、基于角色的数据访问、实体图的查询等等。

下面我们看看 SDK 里面到底包含了什么。

数据模型

应用程序的数据模型在 SDK 中是一组 TypeScript 的类。以快速开始中的 Session Planner 为例,Java 中有一个实体:

@NamePattern("%s %s|firstName,lastName")
@Table(name = "SESSIONPLANNER_SPEAKER")
@Entity(name = "sessionplanner_Speaker")
public class Speaker extends StandardEntity {
   @NotNull
   @Column(name = "FIRST_NAME", nullable = false)
   protected String firstName;

   @Column(name = "LAST_NAME")
   protected String lastName;

   @Email
   @NotNull
   @Column(name = "EMAIL", nullable = false, unique = true)
   protected String email;
//Setters and getters here
}

然后在 SDK 中,会生成一个对应的类:

export class Speaker extends StandardEntity {
   static NAME = "sessionplanner_Speaker";
   firstName?: string | null;
   lastName?: string | null;
   email?: string | null;
}

所有的数据关联关系和组合关系也都会保留,因此可以直接获取实体关系图而不需要调用多个 API 一个一个的获取各个实体了。

另外,这里也没有 DTO,获取到的数据跟后台描述的一样。

业务逻辑服务

在 SDK 中,所有开放的 CUBA REST 服务都会在 TypeScript 中得到体现。比如,如果我们用 REST API 开放了 Session Service 的接口,TypeScript 中的代码会是这样:

export var restServices = {
   sessionplanner_SessionService: {
       rescheduleSession: (cubaApp: CubaApp, fetchOpts?: FetchOptions) => (params: sessionplanner_SessionService_rescheduleSession_params) => {
           return cubaApp.invokeService("sessionplanner_SessionService", "rescheduleSession", params, fetchOpts);
       }
   }
};

可以在前端代码中这样调用:

restServices.sessionplanner_SessionService.rescheduleSession(cubaREST)({session, newStartDate}).then( (result) => {
   //Result handling
});

很方便吧?繁杂的 routine 工作都通过生成的代码完成了。

通用 API

如果自定义逻辑只需要在前端实现的话,可以直接使用在核心 CUBA 平台 REST 库中定义的一组功能,例如:

loadEntities<T>(entityName: string, options?: EntitiesLoadOptions, fetchOptions?: FetchOptions): Promise<Array<SerializedEntity<T>>>;
deleteEntity(entityName: string, id: any, fetchOptions?: FetchOptions): Promise<void>;

这些方法提供了对应用程序中实体 CRUD 粒度的访问操作。访问的安全性也依然存在,CUBA会在服务器端验证所有非匿名调用,并防止用户获取不符合其角色的实体或属性:

cubaREST.loadEntities<Speaker>(Speaker.NAME).then( (result => {
    //Result handling
}));

通过使用这些通用的 API,开发人员可以用 JS 在基于实体 CRUD 之上创建自定义的 API 层应用程序,并将其部署至 Node.js,以实现“服务于前端的后端”架构模式。而且,用这种方式创建的 API 层可以是多个,比如我们可以为不同的客户端创建不同的 API 层:ReactJS,原生 iOS等等。这种用例场景下,自动生成的 SDK 是最理想的工具。

一般情况下,通用 API 的不足之处在于,获取的属性超过了所需的属性或者API描述符中的属性不足时,可能会导致数据获取不足或获取过多的风险。 但是,CUBA 的实体视图在后端解决了这个问题,我们为前端开发人员也提供了同样的方法! 对于每个生成的TypeScript类,我们创建能反映视图的类型:

export type SpeakerViewName = "_minimal" | "_local" | "_base";

export type SpeakerView<V extends SpeakerViewName> = 
V extends "_minimal" ? Pick<Speaker, "id" | "firstName" | "lastName"> : 
V extends "_local" ? Pick<Speaker, "id" | "firstName" | "lastName" | "email"> : 
V extends "_base" ? Pick<Speaker, "id" | "firstName" | "lastName" | "email"> : 
never;

因此,可以从后端获取实体并且只获取后端视图指定的属性。这样的话,不需要去猜测到底拿到了哪些属性,IDE 会自动帮你补全:

text

API 更新

如之前所述,代码生成只是开发过程中的很小一部分。代码改动和持续的支持才是耗费时间的地方。CUBA 的 TypeScript SDK 生成器在随后的运行期间会分析代码,跟踪改动并进行增量更新。 如果您将 TypeScript 作为前端的主要开发语言,TypeScript 编译器也会确保您不会忘记更新使用 SDK 的自定义代码。

总结

除了基于 Vaadin 的通用 UI 之外,如果您还想为 CUBA 应用程序开发基于 JS 的客户端(React / React Native,Angular 或 Vue 等),则可以使用 REST API 扩展插件和 TypeScript SDK。通过使用 TypeScript SDK,不论使用何种前端技术,您都可以只关注设计或者性能,以便提供最佳的用户体验,而不需要做重复冗余的工作。并且几乎可以肯定 JS 和 Java 之间的通信以及支持 API 变化将会是您开发过程中最小的问题。

了解 CUBA 平台,请访问 CUBA 中文官网。