logo

鱼肚的博客

Don't Repeat Yourself

Terser项目中与Typescript主要的不兼容点及改进方向

Terser源自Uglifyjs,其开发时间较早,从现在的ES6、Typescript的视角看过去,存在不少的问题,代码结构存在一些可优化点。

这篇文章中,我主要讲一下在我眼中Terser项目中的问题,及相应的改进方向。

问题

类定义松散,不利于添加类型

terser中的类定义是这样的过程:

  • 先定义一个基本的类,只包含骨架和简单方法
  • 在各个分散的文件中,通过修改prototype的方式,为相应的类添加方法

按这种方式,类在一开始定义的时候,不包含相应的属性和方法,而在import某个文件之后,通过修改prototype,又变成了拥有此属性和方法。

在Typescript中,如果直接声明此类具备相应的方法,则会报类型错误。而如果声明这些方法是可选方法,则在使用的时候需要做各种非空判断,比较麻烦。

改进方向

为了解决这个问题,我在为terser添加类型之前,先对terser进行了一次大的重构。从原来的同类方法定义在一个文件中,改成同一个类的方法定义在一个文件中。

代码不符合es6规范

terser的代码仓库中大量充斥着es5的代码,如很多对prototype的操作,这样的代码在Typescript中都会比较难以处理,带来大量的类型问题。

改进方向

将相应的es5代码先转换成可正常工作的es6代码。

大量的工厂方法

terser中有大量的工厂方法,如 DEFNODE用来创建类,def_optimize用来在各个类中添加optimize方法。

DEFNODE中基于用传参的方式,动态地定义各种函数、方法,在Typescript中基本无法处理。

这些工厂方法增大了添加类型的难度。

改进方向

将相应的工厂方法删除,换成正常的定义。

大量的 instanceOf

instanceOf在typescript中是一种type gurad,可以用来判断类型。但是它有一个缺点,就是必须将相应的class引入。

在terser中,存在着大量的根据子类的类型,决定父类中函数的具体行为的语句。这就导致了如下的问题:

  1. 若是使用 instanceOf,能正常推导类型,但是会带来大量的循环依赖问题。且将来转到Rust/C++等静态语言时,会比较难处理。
  2. 若是不使用instanceOf,换成别的方式实现。则没有类型推导,不方便将类型从any换成具体类型,否则会报一些类型不匹配的问题。

改进方向1

首先将instanceOf换成了一个通用的实例方法 isAst,解决循环依赖的问题。

如原来node instanceOf AST_Class 现在可以换成node?.isAst?.('AST_Class'),这样就没必要引入AST_Class类型,也就不会有循环依赖了。

改进方向2

换成isAst可以解决循环依赖问题,但是没办法解决类型推导问题。原来 instanceOf能起到type guard的作用,现在的isAst方法没有了。

要解决这个问题,可以为isAst方法添加类型推导能力,即

isAst<T extends AST_Node> (type: string): this is T

但是这样还是会需要引入AST_Node,一个替换方案是将所有的AST类的定义,抽取成接口:

1// types.ts
2interface IAST_Node {}
3interface IAST_Class extends IAST_Node {}
4
5// ast/class.ts
6import { IAST_Class } from '../types'
7class AST_Class implements IAST_Class {
8  xxx () {}
9}
10
11// ast/node.ts
12import { IAST_Node, IAST_Class } from '../types'
13class AST_Node implements IAST_Node {
14  foo () {
15    if (this.isAst<IAST_Class>('AST_Class')) {
16      this.xxx()
17    }
18  }
19}

如上代码所示,首先要在一个types.ts中定义所有的ast相关类的定义,然后需要在基类中引入子类的类型,再通过手动的 type guard推导类型。

虽然能用,这样使用起来就比较繁琐了。

改进方向3

第3种改进方向,就是将基类中对 instanceOf 的调用,切换成子类的方法覆盖。

如原来有

1class AST_Node {
2  foo () {
3    if (this instanceof AST_Class) {
4      // code for AST_Class
5    }
6    // code for AST_Node
7  }
8}
9class AST_Class extends AST_Node {}

变成

1class AST_Node {
2  foo () {
3    // code for AST_Node
4  }
5}
6class AST_Class extends AST_Node {
7  foo () {
8    // code for AST_Class
9  }
10}

但是这种方法改起来比较困难,一方面很多判断条件在抽取成方法时不好描述,另一方面容易引入bug。

总结

为了使tsterser开发更加顺利,在为terser添加类型之前,首先要对其进行一次重构,具体来说,主要是将类定义从工厂方法中抽取出来,将动态添加的方法写成静态定义。

另外因为全部抽取之后,所有类都在一起,lib/ast.ts文件变得过大(10000+行代码),还需要进行拆分。