乐趣区

关于javascript:基于-AST-的代码自动生成方案

最近接到了一个需要,须要通过第三方提供的 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 外面的两个事件:

  1. 都定义了什么办法;
  2. 办法里都传入了什么参数。

通过 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 的概念来进一步解决逻辑耦合的问题,心愿能够给读者肯定的启发。

退出移动版