在 Vue2+Js 及原生 Html 项目中使用类型提示

vicat
vicat
发布于 2024-05-22 / 30 阅读
0
0

在 Vue2+Js 及原生 Html 项目中使用类型提示

story

在很久很久以前,Microsoft 在搞出 typescript 之前,使用 原生js 开发网页是一件极为普遍的事情。那时的网页开发规模较小,JQuery 是前端永恒的王者,盒模型 是前端的最佳实践,那时的前端很单纯,从 ajax 请求到后端的数据后就直接塞入 dom 进行展示渲染,若有问题也能很快暴露,后端的接口数据也没有特别复杂。这样简单的生活在到了 Vuetypescript 出现,双向绑定及强类型横行前端后,一切都变了。在强类型重塑了前端思维,前端终于认识到类型系统为前端编码的易用性带来革命后,就再也回不到当初那个字段全靠猜,点出什么算什么的时代了。但是在实际生活中,有很多船大难掉头的项目,这种项目想用上类型提示几乎是很难的。如果能在这种项目中可以知道类型,那前端的编码得有多快乐,多幸福啊!

正好在最近,开源框架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,然后在前端做好绑定,那不就完全可以做到全链路的类型标注了吗?所以我想要的流程是这样的:

那么流程中有几个问题涵待解决

  1. 后端的这些 dto 怎么才能生成类型描述?

  2. 生成的类型描述该是什么格式?

  3. 前端怎么将这些类型描述进行引用和操作?

  4. 前端怎么去根据给定的类型进行提示?

  5. 我的 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)。该项目可以用于 java8java17,通过 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)

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

vojtechhabarta/typescript-generator: Generates TypeScript from Java - JSON declarations, REST service client (github.com)

总结

到此,我们终于体会到了类型系统提供的优秀代码检查与代码提示,以及与后端保持同步的一致性。快乐前端,快乐编程。

不过这套内容还是有缺陷的,因为 typescript 这套东西是微软搞出来的,所以和 vscode 比较契合,在 webstorm 中效果并不是特别好。望注意。

后记

这篇文章前前后后编写了也有快半年了,主要是自己在生活上太懒散。这套简单的体系确实是很好用,为我和我们前端的对接省去了不少精力,也能提前发现一些拼写错误,我个人是对这套方案比较满意的。只不过在公司里面没有推广开。其实后端生成 d.ts 的这套流程在编写 ts 代码时候也是很有必要的。作为一项技术储备也是嘎嘎的。


评论