story
在很久很久以前,Microsoft
在搞出 typescript
之前,使用 原生js
开发网页是一件极为普遍的事情。那时的网页开发规模较小,JQuery
是前端永恒的王者,盒模型
是前端的最佳实践,那时的前端很单纯,从 ajax
请求到后端的数据后就直接塞入 dom
进行展示渲染,若有问题也能很快暴露,后端的接口数据也没有特别复杂。这样简单的生活在到了 Vue
和 typescript
出现,双向绑定及强类型横行前端后,一切都变了。在强类型重塑了前端思维,前端终于认识到类型系统为前端编码的易用性带来革命后,就再也回不到当初那个字段全靠猜,点出什么算什么的时代了。但是在实际生活中,有很多船大难掉头的项目,这种项目想用上类型提示几乎是很难的。如果能在这种项目中可以知道类型,那前端的编码得有多快乐,多幸福啊!
正好在最近,开源框架GitHub - sveltejs/svelte: Cybernetically enhanced web apps抛弃了 ts
转向了 js
,让我意识到了这种事情的可能性。具体的技术细节请查看 还没用熟 TypeScript 社区已经开始抛弃了😭 - 掘金 (juejin.cn)。
场景
博主是一名纯纯的后端 Java
程序员,在工作中从事 CRUD
的工作。在业余开源项目 vicat47/wechat-bot-client (github.com) (License:Unlicense license)使用 node + typescript
后,深深的发现了 typescript
对于 javascript
的提升到底有多大,于是在我作为公司的『全干工程师』进行前端开发时,被 js
的无类型给折磨到了。明明后端简单的一个 .
号,编辑器就能猜出我要访问什么属性,进行什么操作,在前端我却要吭吃瘪肚的复制请求 url
,然后在 ide
中搜索自己需要的 controller
,在点进去自己编写的 dto
,才能查看到我这个接口到底有哪些值,哪些属性。在一些比较麻烦的取数操作和复杂对象下,甚至要查两到三遍!实在是太麻烦了,自己写的接口尚且如此,如果是其他人写的接口那不更加爆炸!于是,我想要找出了一套方案,能够让我在前端开发时,直接获取到对象的类型,岂不美哉!
解决方案
既然前端请求到后端的接口都是我们后端的 dto
生成的,那么我们明显可以将后端的 dto
转换成 typescript
中的 interface
,然后在前端做好绑定,那不就完全可以做到全链路的类型标注了吗?所以我想要的流程是这样的:
那么流程中有几个问题涵待解决
后端的这些
dto
怎么才能生成类型描述?生成的类型描述该是什么格式?
前端怎么将这些类型描述进行引用和操作?
前端怎么去根据给定的类型进行提示?
我的
javascript
项目引用这些typescript
的东西真的会生效吗?
Q:为什么选择 d.ts 的形式?
A:d.ts 是前端公认的类型标准,就算是 js 项目一样可以使用
后端生成 type.d.ts
在后端项目中将 dto
转换为 typescript interface
使用到了这个开源项目:vojtechhabarta/typescript-generator: Generates TypeScript from Java - JSON declarations, REST service client (github.com)。该项目可以用于 java8
和 java17
,通过 maven
插件的形式在编译期生成 typescript interface
。 更加具体的功能可以查看项目的文档,这里给出一个最小实践。
一、在 dto
所在包中添加插件
在你 dto
所在模块的 pom.xml
中添加如下插件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test</artifactId>
<groupId>top.vicat</groupId>
<version>1.5.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-typedef</artifactId>
<dependencies>
<!-- 项目的依赖项,这里不做体现 -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.3</version>
<configuration>
<show>private</show>
</configuration>
<executions>
<execution>
<id>xml-doclet</id>
<phase>process-classes</phase>
<goals>
<goal>javadoc</goal>
</goals>
<configuration>
<doclet>com.github.markusbernhardt.xmldoclet.XmlDoclet</doclet>
<additionalparam>-d ${project.build.directory}</additionalparam>
<useStandardDocletOptions>false</useStandardDocletOptions>
<docletArtifact>
<groupId>com.github.markusbernhardt</groupId>
<artifactId>xml-doclet</artifactId>
<version>1.0.5</version>
</docletArtifact>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>cz.habarta.typescript-generator</groupId>
<artifactId>typescript-generator-maven-plugin</artifactId>
<version>2.37.1128</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
<phase>process-classes</phase>
</execution>
</executions>
<configuration>
<jsonLibrary>jackson2</jsonLibrary>
<classPatterns>
<pattern>top.vicat.dto.*</pattern>
<pattern>top.vicat.dto.response.*</pattern>
</classPatterns>
<javadocXmlFiles>
<file>target/javadoc.xml</file>
</javadocXmlFiles>
<outputKind>module</outputKind>
</configuration>
</plugin>
</plugins>
</build>
</project>
根据上面的 pom 文件中所示,这里添加了两个插件:
maven-javadoc-plugin
:绑定到javadoc
阶段typescript-generator-maven-plugin
:绑定到了generate
阶段。
下面分别解释这两个 maven
插件是干什么的
typescript-generator-maven-plugin
这个插件就是上面所述的生成 typescript interface
的插件,配置好后通过 mvn package
命令执行操作,并在 target
目录下生成 typescript-generator/[包名].d.ts
文件。对于该插件匹配什么目录,是通过 classPatterns
配置项进行匹配的,每个类都生成了一个到多个 interface
,会包括类与类之间的继承关系。
maven-javadoc-plugin
在生成的目标文件中,我同时还想要所有标记在类/字段上的 javadoc
注释,这样我在前端引用时也能很快知道这个类的类型和内容,这里涉及到 java
的编译过程,在编译期我们所有的文档注释已经被剔除,所以直接使用 typescript-generator-maven-plugin
并不能拿到我们字段上的 javadoc
注解,所以只能引入这个插件来将 javadoc
先生成为 javadoc.xml
文件,再在生成类型文件时使用该 xml
文件中的内容。
警告
如果是使用了
lombok
等插件,并将注释打在了字段上,该插件仅会搜索位于字段getter
上的javadoc
。所以需要配置提取private
字段上的注解,请按需使用。<configuration> <show>private</show> </configuration>
最终生成文件
在配置好如上插件后,即可使用 mvn package
生成目标文件,该文件名称为 [模块名].d.ts
。
/**
* xxx第三方接口调用异常记录对象Dto
* @author: vicat
* @date: 2023-07-17
*/
export interface TestInterfaceRecordDto extends Serializable {
/**
* 主键id
*/
id: string;
/**
* 接口名称
*/
name: string;
/**
* 业务描述
*/
businessName: string;
/**
* 接口编码
*/
code: string;
/**
* 任务状态;0失败,1成功默认0
*/
state: number;
/**
* 失败原因
*/
reason: string;
/**
* 失败次数
*/
failNum: number;
/**
* 请求类型
*/
requestType: string;
/**
* 请求url
*/
url: string;
/**
* 请求参数json字符串
*/
jsonParam: string;
/**
* 排序号
*/
sortNumber: number;
/**
* 删除标志
*/
delFlag: number;
contentType: string;
}
// 下面会有无数个 interface
export interface xxxxxx extends xxxxxx {
// 下面内容省略
......
}
到这里,后端生成的 dto
类型描述就结束了。剩下后续的工作是前端的工作。
二、在前端使用类型提示(原生也能用)
在纯 js
的前端项目中要想使类型提示生效,需要使用到 jsdoc
的一部分能力。
书自此处,建议先行去阅读这篇文章,介绍的很详细:JSDoc 真能取代 TypeScript? - 知乎 (zhihu.com)
我这里就简单说说关键需要的能力:
通过编写
jsconfig.json
开启js
中类型检查通过定义
d.ts
文件供jsdoc
引入使用在文件中使用定义的类型。
2.1 jsconfig.json
jsconfig.json Reference (visualstudio.com)
Visual Studio 中的 JavaScript 语言服务 ·微软/TypeScript Wiki (github.com)
如果你了解 tsconfig.json
,那 jsconfig.json
你也一定是手到擒来。这个文件主要是开启一些别名、启用 js
类型检查所提供的。一个典型的 jsconfig.json
文件如下:
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/xxxx/components/*"],
"@manage/*": ["src/xxxx/manage/*"],
"@bigscreen/*": ["src/xxxx/bigscreen/*"]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
],
"allowJs": true
},
"exclude": ["node_modules", "dist", "build"]
}
也就是说有了 jsconfig.json
,才会有 JavaScript Language Service 的一些功能。
2.2 定义 xxx.d.ts
在上面后端的步骤中我们生成了一些 d.ts
文件,这些文件可以按需放在指定的目录下面。然后可以给项目根目录下放置 index.d.ts
定义一些全局使用的类型,比如:
declare interface ResponseResult<T> {
code: number;
msg: string | null;
data: T;
}
/**
* 分页数据
*/
declare interface PageData<T> {
dataList: T[];
totalCount: number;
}
declare interface PageQuery<T> {
page: PageParam;
filter: T;
}
/**
* 分页参数
*/
declare interface PageParam {
pageNum: number;
pageSize: number;
}
可以看到在里面使用了一些泛型,抽象出来以后确实是这样的。
2.3 开始编码!
简单举几个例子大伙看看
/**
* 获取相机位置
* @returns {Promise<ResponseResult<import("./xxApi").CameraSimpleResponse[]>>}
*/
export function getCameraPosition() {
return request({
url: "/screen/position/camera",
method: 'get',
});
}
/**
* 获取每日任务列表
* @param {"running" | "scheduled"} type 类型,分别为计划任务和执行任务
* @returns {Promise<ResponseResult<import("./xxApi").TaskDto[]>>}
*/
export function getTaskListDaily(type) {
return request({
url: '/screen/task/list/daily',
method: 'get',
params: {
type: type,
},
});
}
getTaskListDaily("running").then((res) => {
if (res.code == 200) {
this.taskOptions = res.data.map((item) => {
return {
value: item.id,
label: item.name,
};
});
}
});
到此就大功告成!享受类型系统为前端带来的爽快感吧!
参考文献
TypeScript JSDoc 应用及其局限性 - 知乎 (zhihu.com)
JavaScript 如何拆分 JSDoc 以及虛擬加載 TypeScript 定義檔 | by Lastor | Code 隨筆放置場 | Medium
JSDoc 真能取代 TypeScript? - 知乎 (zhihu.com)
JavaScript Language Service in Visual Studio · microsoft/TypeScript Wiki (github.com)
javascript - Reference a TS interface from jsDoc - Stack Overflow
Type hints on pure .js files - DEV Community
总结
到此,我们终于体会到了类型系统提供的优秀代码检查与代码提示,以及与后端保持同步的一致性。快乐前端,快乐编程。
不过这套内容还是有缺陷的,因为 typescript
这套东西是微软搞出来的,所以和 vscode
比较契合,在 webstorm
中效果并不是特别好。望注意。
后记
这篇文章前前后后编写了也有快半年了,主要是自己在生活上太懒散。这套简单的体系确实是很好用,为我和我们前端的对接省去了不少精力,也能提前发现一些拼写错误,我个人是对这套方案比较满意的。只不过在公司里面没有推广开。其实后端生成 d.ts
的这套流程在编写 ts
代码时候也是很有必要的。作为一项技术储备也是嘎嘎的。