如花是前端开发同学,精通各种框架和JS工具链,雷卷是Java程序猿,对Spring Boot比较熟悉而已,他们经常配合做一些项目开发。一天
产品经理
要求开发一个新的Social项目,准备试水一下, 项目之旅由此开始啦。
前后端分离
如花主要负责前端,如界面、交换、REST API 调用等,雷卷主要负责后端,数据库、Redis、输出REST API等,分工非常明确。 这没有什么难的,访问 start.aliyun.com/
创建项目,然后编写对应的Controller,输出REST API, 第一个Controller不到5分钟就出炉啦,如下:
@RestController
@RequestMapping("/user")
@CrossOrigin("*")
public class UserController {
@GetMapping("/nick/{id}")
public Mono<String> findNickById(@PathVariable("id") Integer id) {
return Mono.just("nick: " 1);
}
}
Copy
雷卷钉钉给如花,告诉该REST API如何调用。 如花也很迅速,创建一个Node项目,然后webpack等都设置好,他选择了axios这个package,告诉雷卷这个JS的http client多么好,什么浏览器和Node自适配,支持Promise,而且API简洁,一个demo,确实非常简单。
const axios = require('axios').default;
(async () => {
let userId = 1;
let nick = await axios.get('/user/nick/' userId)
.then(function (response) {
return response.data;
})
console.log(nick);
})()
Copy
OpenAPI 3来啦
没过几天,REST API已经有十几个啦,钉钉发来发去,已经不行啦。 经常URL要调整、参数要变更、返回的json对象字段也在调整,钉钉上说不明白,而且如果有其他同学加入怎么办? 没有文档说明。
他们决定使用OpenAPI 3规范,只要有该规范,如花就不用钉钉雷卷,只要参考标准OpenAPI文档就可以啦。 这个也不麻烦,当然不会手写这个规范的json,有springdoc-openapi项目,可以直接输出OpenAPI规范文件,非常easy。 pom.xml中添加一下依赖,如下:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webflux-ui</artifactId>
<version>1.3.0</version>
</dependency>
Copy
然后对应的Controller方法上添加一下@Operation进行说明,就可以啦。
@GetMapping("/nick/{id}")
@Operation(description = "find nick by id")
public Mono<String> findNickById(@PathVariable("id") Integer id) {
return Mono.just("nick: " 1);
}
Copy
代码稍作调整,雷卷将OpenAPI规范地址发给如花,http://xxxx:8080/swagger-ui.html ,你自己根据看这个规范就可以啦。 界面如下:
如花中途找到雷卷,主要是描述完善、增加tag等,方便定位等。 在此过程中,如花有点受伤,API一旦调整,雷卷就会钉钉给如花,XXX API调整,你OpenAPI Console上看一下新的规范,你自己调整代码就可以啦。
OpenAPI不那么简单
API越来越多,已经有50个啦,一天如花来找到雷卷,讨论一下HTTP请求约定的问题。 如花抱怨到,现在的这些REST API,太灵活啦,一些参数要放到URL的path中,一些参数要在query中,还有一些是放在http header中,GET和POST不停的在切换,一些API返回的是text/plain,一些返回的是json,虽然OpenAPI上也多明确说明啦,如花还觉得自己非常多的时间都花在构建axios请求的config对象上啦,而且代码非常不好看,到处都是这些config对象的构建,而且这些参数和返回值类型,都没法做代码提示,一次不小心把参数名写错啦,花费了一个上午来调试才找到。 ”你钉钉上说一句API调整啦,我要花半天看哪些调整啦,又不能代码diff,
IDE
又不能提示,我眼睛都累死啦。” 如花有一堆的抱怨。
雷卷告诉如花, 你根据OpenAPI规范生成Node.js代码不就可以啦,json文件就在那里,自己搞呗。 你知道的,我也没法统一,token必须在header中,考虑URL友好型,一些变量放在URL的Path中还是有道理的,还有一些安全考量,没有办法,同时还有性能要求,Servlet Filter规范等,这就是HTTP。
如花铁了心不想看什么OpenAPI规范,对照着写这些无聊的代码,API调整后还要去找不同,简直是拿前端不当人。和雷卷讨论一上午,即便有OpenAPI规范,要从OpenAPI json中自动生成这些固定格式代码,还是有些麻烦,需要后端配合,输出非常多的信息,才能做到代码提示等需求。而且如何配合webpack自动化这个流程也麻烦,下载最新的OpenAPI json,调用脚本生成js代码,也不方便自动化等。
让Spring Boot生成npm package
最后两个觉得能否让Spring Boot应用自动生成一个npm package,然后如花在代码调用该package提供的API就可以。package.json添加一个依赖,该依赖关联到Spring Boot应用提供的URL,如下:
"dependencies": {
"@UserService/UserController": "http://localhost:8080/npm/@UserService/UserController"
}
Copy
接下来就是要定服务接口啦。既然JavaController已经提供了API,为何不复用该API作为Service API,反正不能让如花再构建什么http请求,写那么多axios无聊代码。 最终结论如下:
- JavaScript的代码应该这样:
const userController = require("@UserService/UserController");
(async () => {
let nick = await userController.findNickById(1);
console.log(nick);
})()
Copy
- TypeScript的代码应该这样:
import userController from "@UserService/UserController"
userController.findUserById(1).then(user => {
console.log(user.nick)
});
Copy
基于接口通讯,你就是想查找会员nick,那么findNickById(id) 这个是最好的API,鬼才想知道背后使用什么技术实现的,以后URL调整,参数调整,http还是https,哪怕是HTTP/2,都不能让我改什么代码。
Spring Boot生成npm package背后的技术
技术上完全没有问题,而且相当靠谱。 对于REST Controller,我们已经使用@GetMapping, @PostMapping, @PathVariable, @RequestParam,@RequestBody等,这些元信息已经存在啦,我们只需要找到对应的Controller Spring Bean,然后使用反射机制将这些信息转换为JS Code代码就可以啦。
一些技术实现点:
- npm package的核心文件三个: package.json、index.js和index.d.ts,package.json不用说,index.js主要是根据controller生成的js代码,而index.d.ts是给TypeScript用的。
- package是tar.gz格式的,package.json和index.js生成后,调用commons-compress输出tgz格式
- index.js要整合axios,核心一个JS Class, Java Controller Method到 JS class的method,最后是融合axios的一些代码。
- 处理JSDoc,要能自动生成对应的tags,方便进行代码提示,同时IDE也能进行验证和纠错。
- 一些缺失的信息,调用OpenAPI的annotion补全,如@Operation, @ApiResponse 等。
详细代码实现,如何从Java是生成JS,类型如何匹配等,这里不细说,这里有一个问题要澄清一下,对于生成的代码,主要是强烈流程正确,而不太讲究代码的可阅读性,这些生成的代码都是给机器阅读的,如你看protobuf生成的代码,没有人能读懂。 但是我会竭力将代码可阅读性提供,毕竟还有一些调试问题。
让我们看一下最终的效果。
最终效果
- 代码提示完美啦,函数提示和参数提示都没有问题
- 返回值提示也支持,主要是 JSDoc的@typedef 在起作用
- 函数中参数对象的属性值也能提示,想犯错都不那么容易
通过Spring Boot生成npm package和基于服务接口调用,将所有的底层细节全部屏蔽。 如花不用再去看那个OpenAPI控制台,直接调用接口就可以, 也不问这个参数是path变量,还是请求变量,还是在http header中的name,这些全部不用考虑,至于是GET还是POST,更不要考虑。 所有的这些细节都从后端的Controller直接生成即可。
当然特性还不止这些,包括OpenAPI的annotation整合,JS函数的optional,default value支持,@Deprecated API废弃等,当然TypeScrip支持也是第一位的。总结一下,调用更简单,而且全面的审查和代码提示。
虽然项目还是提供OpenAPI的swagger-ui.html,但是如花基本不怎么去看啦,代码编写这么简单,看那个Swagger UI干什么,虽然swagger控制台可以做REST API测试,但是怎么比得过jest来的简单和快捷。
test('findNickById', async () => {
let nick = await userController.findNickById(1);
console.log(nick);
});
Copy
考虑到一些同学可能不知道这个HTTP URL地址,这些如花也想好啦,提交到npm repository就可以,生成的package.json中version是基于时间点的,我们测试完成后,命令行自动提交一下就可以啦,其他同学想用,就是标准的package name就可以。
总结
最后说明一下, 这个项目的灵感来自于Vaadin两天前的blog,不少同学都在使用TypeScript编写浏览器端应用,那么TypeScrip如何后后端通讯?
最好的方式当然是import后直接API调用,而不是如何封装XMLHttpRequest,提供更便捷的API,axios已经提供非常好的API啦。 基于接口的代码如下:
import * as helloEndpoint from './generated/HelloEndpoint'
async makeCall() {
const greeting = await helloEndpoint.sayHello('Vaadin');
console.log(greeting);
}
Copy
Java服务端的代码如下:
@Endpoint
public class HelloEndpoint {
public String sayHello(String name) {
return "Hello " name;
}
}
Copy
这种基于接口方式写代码非常简洁,完全是无脑的那种。 Vaadin能做到这点,主要就是在编译过程中,如HelloEndpoint生成对应的npm package,然后提供给TS调用。
Spring Boot npm package generator是在运行期生成,node开发同学不用安装JDK,执行Maven编译,然后将npm package拷贝过来等,现在只需一个依赖声明就可以。
借鉴于这个思想,RSocket的npm package也是自动生成。如果我是前端同学,我只想调用一下findNickById(id)实现功能,至于底层通讯是REST API, gRPC还是RSocket,管我什么事情。 即便再好的http client封装,也没有接口约定来的最直接。
当然如果是Python,Ruby等语言,可以使用同样的机制,完全为开发者屏蔽各种http client, gRPC client, RSocket Client等。
原文链接:https://www.tuicool.com/articles/y2uURn3
,