TypeScript简介
TypeScript(TS)是一种强类型的编程语言,由于在编译时便会发现代码存在的问题,被前端团队大量使用。
TS使开发人员能够掌握各种组件的交互方式,并使代码重构变得更加容易;但对于需要准确定义的类型,在使用时会比较麻烦。特别是涉及数个及数十个后端接口数据的类型定义,会有大量的转换和的时间成本。通过VSCode插件实现一键生成TS声明,可以节省编程过程的开发成本。
Auto-ts-inline-types简介
为了克服TS的变量类型声明过于繁琐的缺点,节约开发成本,推荐使用VSCode插件【Auto-ts-inline-types】。它可以通过可视化窗口,模拟一个网络请求。通过接口返回的信息,或者手动修改后的信息,自动生成对应的TS声明。
此文章主要介绍插件中Json对象生成TS声明的转换原理。
转换原理
(一)生成所有的interface对象
- 首先对需要转换的Json对象进行遍历,遍历对象所有属性。当遇到属性key/value中的value是object的情况,则开始递归,以此循环。
- 当某个对象遍历后,发现所有属性的value都是基本类型,则生成typeDescription(包含HashId和typeObj)。
(备注:HashId为引入第三方包Hash后,通过将typeObj转化为HashId而来,通过HashId保证了typeDescription的唯一性)
// 数据结构示例:
const HashId = 'cc1c310866bbc757b533d7650c9e9934b2e47bf9';
const typeObj = {
name: string;
version: string;
description: string;
main: string;
author: string;
license: string;
}
- 递归开始由内层向外层开始依次return,最后返回的typeStructure中包含rootTypeId和types(包含所有生成的typeDescription)。
(备注:如下图所示,在生成的types数组中包含所有的interface声明。types中每个元素的的HashId属性用来维护元数据Json对象的层级关系。但在types中,所有对象都是平级的,因为生成的interface都是平级的。在后续的转换过程中,会将HashId替换为变量名,以此生成TS声明)
- 如果发现某个对象的属性中,存在某些value为数组类型,则对数组进行遍历递归。
Value是数组的情况有以下5种:
序号 | 元素类型 | 生成的变量类型 |
1 | 全部是数组 | (List1 | number | string)[][] |
2 | 全部是数组(包含null) | (List1 | number | string | null)[][] |
3 | 全部是对象 | List2[] |
4 | 全部是对象(包含null) | (List2 | null)[] |
5 | 多元素类型 | (List1 | (number | string)[] | number[])[] |
对于数组的typeDescription结构体如下图所示。【arrayOfTypes】代表该数组中的所有元素类型;【isUnion】代表是上面的表格中,元素类型的第5种情况(多元素类型)。
(二)生成对应Names数组
把接口返回的信息转化为rootTypeId和types组成的对象后,就需要通过HashId和types的对应关系,找到HashId对应的Key,再将Key的首字母变为大写,生成interface中的Name。维护好一份HashId和Name的Json对应关系后,存放于数组Names中,在转换过程的最后进行替换。
- 首先判断由Json对象转换而来的TypeStructure对象的结构,取出RootTypeId。在types数组中找到RootTypeId对应的typeDescription,遍历typeObj。
- 根据typeObj的数据结构,通过arrayOfTypes来判断是否是数组。
- 如果是数组,则生成对应的类型。
- 如果是对象,则自动生成对象(包含HashId与name),放入names数组中。
(三)根据HashId映射生成TS声明
- 通过HashId做映射,将Names数组中的name作为interface的名称。
- 在types数组中,根据HashId寻找每个interface中的属性。如果存在属性的value是HashId的,则在Names中查找对应的name。
- 将生成的interface对象,变换为字符串的形式,包含interface以及JSDoc注释。
/**
* @description 构造interface结构体
*/
function getInterfaceStringFromDescription(_a) {
var name = _a.name, typeMap = _a.typeMap;
// 构造interface内部结构
var stringTypeMap = Object.entries(typeMap)
.map(function (_a) {
var key = _a[0], name = _a[1];
return " " + key + ": " + name + ";n";
})
.reduce(function (a, b) { return (a += b); }, "");
// 注释说明
var descriptionName = `/**n *@description ${name}n`
// 属性类型
var descriptionTypeMap = Object.entries(typeMap)
.map(function (_a) {
var key = _a[0], name = _a[1];
return " *@param {" + name + "} " + key + "n";
})
.reduce(function (a, b) { return (a += b); }, "");
// Doc注释
var descriptionString = descriptionName + descriptionTypeMap + \' */n\'
// 构造interface结构体
var interfaceString = "interface " + name + " {n";
interfaceString += stringTypeMap;
interfaceString += "}";
return descriptionString + interfaceString;
}
(四)生成JSDoc注释
可以在接口返回信息中,直接通过注释来添加每个字段的含义。
- 收集所有的用户手动添加的注释信息,生成注释信息json结构体。
- 在接口的返回信息中,去掉用户的注释,以使返回的文本信息可以转换为Json结构体。
- 在转换的结果中,替换掉已经注释含义的字段,并写入生成的JSDoc注释中。
/**
* 将中文含义写入JSDoc注释
*/
const getFinalInterface = (text: string) => {
for (let key in commentJson) {
// commentJson是所有的字段和中文含义的对应关系
text = text.replace(key + \'n\', key + \' \' + commentJson[key] + \'n\');
text = text.replace(
key + "\'" + \'n\',
key + "\'" + \' \' + commentJson[key] + \'n\'
);
}
return text;
};
问题总结
- 自动生成的interface对象的顺序为递归的顺序,json对象的最外层interface(RootType)会被写入生成文件的最下面。目前是手动调整顺序,将interface名为RootType的声明置于最顶层,其余字段按照从上到下的顺序排列。
if (name === \'RootObject\') {
// 如果是根对象,则添加至数组头部
nameMap.unshift({ id, name });
} else {
// 如果是其他对象,则在数组后面依次添加
nameMap.push({ id, name });
}
- 如果在接口返回信息中,存在多个相同key名的字段,并且用户手动写了不同的含义。目前JSDoc注释会将所有的key名都注释成相同的含义。