最近接到了一个需要,须要通过第三方提供的 d.ts 文件来定义对应的 JS SDK 文件,其模式如下:
第三方提供的 d.ts 文件:
export class SDK {start(account: string);
close();
init(id: string): Promise<{result: number;}>
}
定义进去的 JS SDK 文件:
// 初始化 wrapper 对象,省略了细节
const wrapper = (wrap) => wrap;
// 定义 JS SDK
const SDK = {async start({ account}) {return await wrapper.start(account)
},
async close() {return await wrapper.close(account)
},
async init({id}) {return await wrapper.init(id)
},
}
export default SDK;
在我的项目初期的时候,咱们是依据第三方提供的 d.ts 文件,手动地去撰写 JS SDK。因为这个 d.ts 常常会变动,咱们须要不停地同步 JS SDK;同时因为咱们的我的项目是多人保护的,手写的 JS SDK 难免会有许多的抵触,这些问题对于研发效率来说都是不利的。
通过剖析 d.ts 及其对应的 JS SDK 能够看出,它们的格局是根本固定的,两者之间也有着十分清晰的对应关系。于是咱们能够思考,能不能通过自动化的办法,间接从 d.ts 里生成对应的 JS SDK 呢?
绝对简略的思路是逐行剖析 d.ts 代码,通过正则等形式去匹配关键字来取得要害信息。这种形式简略粗犷却不够优雅,须要非常复杂的匹配规定能力满足需要,一旦 d.ts 格局有变动,原来的匹配规定兴许会间接无奈应用,保护老本太高。
若要防止因为格局的变动带来的一系列问题,“形象”能够说是一种绝对更适合的计划。而代码的 AST 就是一种形象的形式,它可能无效地防止因格局、写法地变动带来的影响,把源码转化成一份能够不便脚本浏览的树状构造数据,以不便后续的操作。
d.ts 的 AST 剖析
因为 d.ts 也是一个 typescript 文件,因而咱们能够应用 typescript 官网提供的 API 来生成对应的 AST:
// https://ts-ast-viewer.com/
const dTsFile = fs.readFileSync(resolve(__dirname, filePath), 'utf-8')
const sourceFile= ts.createSourceFile(
'sdk.ts', // 自定义一个文件名
dTsFile, // 源码
ts.ScriptTarget.Latest // 编译的版本
)
咱们也能够借助 https://ts-ast-viewer.com 这个网站来查看生成进去的 sourceFile(AST)是否合乎预期:
有了 AST,接下来就须要剖析咱们到底须要外面的什么信息。从前文的 d.ts 到 JS SDK 的例子能够看出,最重要的事件就是要晓得 d.ts 外面的两个事件:
- 都定义了什么办法;
- 办法里都传入了什么参数。
通过 AST 能够晓得,位于 ClassDeclaration
下的 MethodDeclaration
就是该 d.ts 所定义的一系列办法;而 MethodDeclaration
外面的 Parameter
则定义了办法的参数。
接下来是不是就要去读取 AST 的节点信息,而后间接生成 JS SDK 呢?答案是否定的。究其原因,如果把“剖析 AST”和“生成 JS SDK”的逻辑都耦合在一起的话,因为 AST 节点数量多、类型丰盛的特点,可能须要大量的条件判断,最终的逻辑会十分凌乱,有一种“看一点做一点”的感觉,反而和逐行读取 d.ts 而后生成 JS SDK 的思路没什么两样。
为了防止这种过于耦合带来的难以保护的问题,咱们能够引入“畛域特定语言 (domain-specific language)(DSL)”。
应用 DSL 来生成 JS SDK
对于 DSL 的定义,能够参考这篇文章《开发者须要理解的畛域特定语言(DSL)》。DSL 的定义听起来如同很厉害,其实说白了就是自行定义一种能够承前启后的过渡格局。
在咱们的场景中,能够定义一种 JSON 格局的 DSL,用于记录从 AST 中提取进去的要害信息,而后再从这个 DSL 中去生成所须要的 JS SDK 文件。这种形式看起来仿佛多了一步工作,减少了工作量,但理论应用下来会发现其对于逻辑的解耦是十分有帮忙的,对于后续的保护也是一个极大的利好。
对于咱们的例子来说:
export class SDK {start(account: string);
close();
init(id: string): Promise<{result: number;}>
}
通过剖析其 AST,能够整顿成这么一个 DSL:
const DSL = [{
name: 'start',
parameters: [{
name: 'account',
type: 'string'
}]
}, {
name: 'close',
parameters: []}, {
name: 'init',
parameters: [{
name: 'id',
type: 'string'
}]
}]
DSL 外面清晰记录了办法的名称和参数,如果有须要也能够很不便地往里增加更多的信息,如返回值的类型等等。
接下来就是剖析 JS SDK 的格局了:
const wrapper = (wrap) => wrap;
// 定义 JS SDK
const SDK = {async start({ account}) {return await wrapper.start(account)
},
async close() {return await wrapper.close(account)
},
async init({id}) {return await wrapper.init(id)
},
}
export default SDK;
因为格局也是固定的,因而只须要筹备一个字符串模板,而后遍历 DSL,把组织好的办法填到模板就能够了:
const apiArrStr = DSL.map(api => {
// 伪代码,省略了信息提取的步骤
return `
async ${name}(${params}) {return await wrapper.${name}(${params}) }
`
})
const template = `
const SDK = {${apiArrStr}
}
export default SDK;
`
return template;
小结
本文介绍了通过 AST 的形式来剖析 d.ts 代码,进而主动生成对应的 JS SDK 的办法,同时引入了 DSL 的概念来进一步解决逻辑耦合的问题,心愿能够给读者肯定的启发。