最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 讲讲如何实现自动转换ts的import和type-only import

    正文概述 掘金(Freeze_Crow)   2021-02-10   771

    又有半年没写文章了,趁着最近不那么忙先来水一篇。恰巧最近给typescript-eslint实现的支持decorator metadata的PR合掉了,那就拿这个写写吧。标题因为不知道怎么取,就拿最终实现的目标写上去了。

    背景

    先说说做这件事的背景吧。我们公司一小伙苦于项目里的循环依赖,写了个脚本来检测,但是检测归检测并没有给出好的解决方案。我看了下如果要从根本上解决(比如抽离公共依赖之类的方法)解决那肯定少不了一番重构,可是老代码谁也不敢轻举妄动啊。

    刚好上半年把整个组的typescript升级到3.8以上了,我就想3.8新增的Type-Only Imports或许能解决一部分问题。纵观循环依赖的代码,大概有一半以上确实只依赖了类型,比如下面这种很常见的情况

    // Container.ts
    import Element from './Element';
    export default class Container {
      elements: Element[];
    }
    
    // Element.ts
    import Container from './Container';
    export default class Element {
      context: Container;
    }
    

    虽然有从代码上优化的解决方法(比如抽离接口等方法,本文就不说明了),最简单的方法就是改成import type,另外明显后者是可以用工具自动完成的。

    consistent-type-imports规则

    因为这个优化明显可以通过单文件的代码分析来完成,现成的工具里第一个想到做这个的就是eslint了,于是找了找发现果然有现成的规则[consistent-type-imports]。这个规则可以通过配置将文件里只有类型引用的import变量改为import type。

    但是试了下这个规则后发现有个问题,会导致项目里的decorator拿不到metadata,一些依赖类型信息实现的功能就会失效。看了下原因是因为当tsconfig里emitDecoratorMetadata === true时,ts会将带有decorator的类里的成员变量类型、方法参数和返回类型输出到js里。比如下面将成员变量类型输出的情况,会在js里引用该类型,而当前规则无法判断这个情况,将原代码改为import type,这时实际输出的js里就拿不到这个Type了。

    // input ts with emitDecoratorMetadata = true
    import Type from 'type';
    class A {
        @deco
        a?: Type;
    }
    
    // output js ignore some helper function
    import Type from 'type';
    class A {
    }
    __decorate([
        deco,   
        __metadata("design:type", typeof (_a = typeof Type !== "undefined" && Type) === "function" ? _a : Object)
    ], A.prototype, "a", void 0);
    

    什么时候会输出decorator metadata

    为了修复这个问题首先要看看ts在什么时候会输出decorator metadata。 首先能添加到metadata的类型一定只能是个存在对应类的直接类型,任何加工过的类型和type alias都会给你个Object,下面列了一些不能输出或者输出异常的情况。

    type Type = T1;
    class A {
        // 下面decorator全都省略...
        a: T1 | T2; // 直接输出Object
        b: Pick<T1, 'key'>; // 同上
        c: Type; // 会将 Type 输出,但是type alias会被抹去,所以这里并不能引用到T1
        d: React['Component']; // 直接输出Object,使用 React.Component 可以正常输出
        e: (a: string) => string; // 直接输出Object
    }
    

    decorator通常会出现在4个地方:1. 类的定义上;2. 类的成员变量上;3. 类的方法上;4. 类的方法的参数上;这里以这种方式分开讨论。

    类的定义上

    当类的定义上存在decorator时,构造函数的所有参数类型都会输出metadata。

    @deco
    class A {
        // Type会被输出到metadata
        constructor(t: Type) {}
    }
    

    类的成员变量上

    当类的成员变量上存在decorator时,该变量的类型会输出metadata

    class A {
        @deco
        field: Type; // Type会被输出到metadata
    }
    

    类的方法上

    当类的方法上存在decorator时,该方法显式声明的参数类型和返回类型会输出到metadata。另外要注意的是这里指的方法不包括赋值给成员变量的函数。

    class A {
        @deco
        foo(input: Type1): Type2 {} // Type1, Type2会被输出到metadata
        
        @deco
        foo = (input: Type1): Type2 => {}; // 这样子只会被当作普通的成员变量处理
    }
    

    这里有一种特例就是 gettersetter,对于相同名称的accessor,只要给其中一个添加decorator就会输出相关类型的metadata,这个类型以setter参数类型优先。比如下面虽然只给getter加了decorator,却只使用了setter的参数类型Type2作为metadata。

    // source
    class A {
        @deco
        get foo(): Type1 {}
        set foo(v: Type2) {}
    }
    
    // output metadata
    __decorate([
        deco,
        __metadata("design:type", typeof (_b = typeof Type2 !== "undefined" && Type2) === "function" ? _b : Object),
        __metadata("design:paramtypes", [typeof (_c = typeof Type2 !== "undefined" && Type2) === "function" ? _c : Object])
    ], A.prototype, "foo", null);
    

    这里 gettersetter 的metadata会按照一定规则合并,一般标识符类型和非复杂的计算值都是支持的。

    class A {
        // 相同的普通key、字符串、数字字面量和变量可以合并
        get a() {}
        set ['a'](v: Type) {}
        
        get [1]() {}
        set [1](v: Type) {}
        
        // const key = 'k';
        get [key]() {}
        set [key](v: Type) {}
        
        // 其他类型和表达式不可以合并
        get [true]() {}
        set [true](v: Type) {}
        
        get ['a' + 'b']() {}
        set ['a' + 'b'](v: Type) {}
    }
    

    类的方法的参数上

    当类的方法的参数上存在decorator时结果和类方法上有decorator差不多,参数类型和返回类型都会添加到metadata。比如下面两种情况结果是一样的

    class A {
        @deco
        foo(input: Type1): Type2 {}
    
        foo(@deco input: Type1): Type2 {}
    }
    

    同样 setter 上的参数decorator也是个特例,结果是啥也不会输出。这时不仅metadata不会输出,连decorator的功能也不会生效。这大概是个TS的BUG。所以下面这种情况需要特殊判断排除掉

    class A {
        set foo(@deco input: Type1): Type2 {}
    }
    

    如何给typescript-eslint修复

    整理完规则后就是看看如何修复了,具体的代码比较多就不详细说明了,就讲讲涉及的一些模块吧。这里可以直接从consistent-type-imports这个规则的代码入手。

    在代码里可以看到创建规则是通过给createRule 传入几个用来描述规则的参数和承载主要逻辑的 create 函数来实现,create函数返回的是ast visitor,在这里可以实现当前规则所需要的visitor,最后在Program:exit 通过context.report 报告错误信息和修复函数。

    一开始我的想法就是直接在规则代码里通过AST分析判断类型是否会被decorator metadata引用到,拿到最终变量的引用是否全是类型和当前的import类型来判断错误以及修复。后来经过维护者提醒需要将部分判断逻辑实现在scope-manager里,这样对所有规则都是有用的,我也觉得很有道理。

    scope manager是eslint就有的一个概念,它会根据AST生成作用域以及内部的所有变量和引用,eslint-typescript则是重写以支持了TS的语法特性,比如可以通过变量上的references拿到所有的引用然后通过Reference上的 isValueReferenceisTypeReference判断引用类型,在[consistent-type-import]里就可以通过这两点在访问 ImportDeclaration时就能判断是否是正确的import类型。原先的BUG也主要就是scope manager在生成引用类型时没有考虑decorator metadata的影响,那么如何修复就很明显了,在scope manager里生成变量引用时将decorator metadata考虑进来。

    scope manager这块也是写各种ast visitor,每次进入新的scope时要 scopeManager.nestForScope(node) 一下,定义了变量时 currentScope.defineIdentifier(...),引用了变量时 currentScope.referenceValuecurrentScope.referenceType。因为这个需求只和类有关,同时为了方便隔离嵌套类的情况,就独立了一个ClassVisitor,专门处理class scope的生成,不过好像因为独立的这个visitor没有漏了一些ast node的处理后面还导致了一些bug。。。

    总之虽然目前还有一个小问题,但不妨碍使用,可以升到4.14.2愉快使用了。


    起源地下载网 » 讲讲如何实现自动转换ts的import和type-only import

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元