【问题标题】:Strip Typescript decorator in release build在发布版本中剥离 Typescript 装饰器
【发布时间】:2018-09-03 08:30:33
【问题描述】:

我有调试时 Typescript decorator @log,它记录了修饰函数的输入/输出/统计信息。

我想在编译发布版本时完全去掉这个特殊的 @log 装饰器。

从发布版本中删除console.log 语句或在装饰器代码中有条件地执行操作很容易,但我想确保调用装饰器函数本身没有开销。

有什么方法可以用 Typescript 来实现吗?

我的项目是基于 webpack 的。如果 Typescript 无法做到这一点,也许可以在后期使用 Babel 插件、UglifyJS 或其他替代插件来完成?

【问题讨论】:

  • 您也许可以将您的装饰器存储在它自己的文件/模块中,然后对于发布版本,使用该路径/模块名称的别名来代替解析为无操作函数。但是,这确实保留了装饰器,因此它并没有真正回答您的问题。
  • 也可以通过 typescript 转换器插件来完成,该插件明确地完全去除装饰器代码:参见 github.com/cevek/ttypescript
  • 实际上,我从您的指点开始,意识到我完全错过了 Decorator Factory 的工作原理 :)。看来,如果我只是在工厂中返回“未定义”,那么目标函数根本不会被修饰。我将对其进行更多测试,如果它确实像那样工作 - 它是可以接受的。我可以简单地检查工厂中的“DEBUG”变量。加载时间开销很小(在第一次评估 JS 时调用 __decorate 内部函数)。但是对象实例化和调用似乎是“免费的”!
  • @sbat 你最后有什么发现吗?我也不喜欢这种开销。
  • @LppEdd 在我自己接受的答案中我已经解释说 call 本身没有运行时开销。仅在初始化时。这对我的用例来说没问题。

标签: typescript webpack decorator uglifyjs


【解决方案1】:

此编译时转换器从元素和命名导入中删除装饰器。
我会不断更新代码,因为它只是一个(工作)测试。

export default (decorators: string[]) => {
  const importDeclarationsToRemove = [] as ts.ImportDeclaration[];

  const updateNamedImports = (node: ts.NamedImports) => {
    const newElements = node.elements.filter(v => !decorators.includes(v.name.getText()));

    if (newElements.length > 0) {
      ts.updateNamedImports(node, newElements);
    } else {
      importDeclarationsToRemove.push(node.parent.parent);
    }
  };

  const createVisitor = (
    context: ts.TransformationContext
  ): ((node: ts.Node) => ts.VisitResult<ts.Node>) => {
    const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<any> => {
      // Remove Decorators from imports
      if (ts.isNamedImports(node)) {
        updateNamedImports(node);
      }

      // Remove Decorators applied to elements
      if (ts.isDecorator(node)) {
        const decorator = node as ts.Decorator;
        const identifier = decorator.getChildAt(1) as ts.Identifier;

        if (decorators.includes(identifier.getText())) {
          return undefined;
        }
      }

      const resultNode = ts.visitEachChild(node, visitor, context);
      const index = importDeclarationsToRemove.findIndex(id => id === resultNode);

      if (index !== -1) {
        importDeclarationsToRemove.splice(index, 1);
        return undefined;
      }

      return resultNode;
    };

    return visitor;
  };

  return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) =>
    sourceFile.fileName.endsWith('component.ts')
      ? ts.visitNode(sourceFile, createVisitor(context))
      : sourceFile;
};

program.emit(
  program.getSourceFile('test.component.ts'),
  undefined,
  undefined,
  undefined,
  {
    before: [stripDecorators(['Stateful', 'StatefulTwo', 'StatefulThree'])]
  }
);

输入:

import { Stateful, StatefulThree, StatefulTwo } from './decorators';

@Stateful
@StatefulTwo
@StatefulThree
export class Example {
  private str = '';

  getStr(): string {
    return this.str;
  }
}

JS 输出:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Example {
    constructor() {
        this.str = '';
    }
    getStr() {
        return this.str;
    }
}
exports.Example = Example;

【讨论】:

  • 我没有详细测试/审查,但看起来这完全解决了我原来的问题!感谢您研究 Transformer 方法!
  • @sbat 没什么。我仍在学习如何做到这一点。请记住,您仍然不能使用纯tsc 的转换器。我已经在这里github.com/microsoft/TypeScript/issues/33118 对 TS 计划发表了评论
【解决方案2】:

在问这个问题时,我完全忽略了一个令人尴尬的微不足道的方面。每个方法声明只调用一次装饰器函数本身。如果在初始化时将装饰器评估为无操作函数,则开销只会在初始化时发生,并且它将非常小,如下面的代码所示。

@log 装饰器标记的函数的类实例化和运行时函数调用将没有任何开销。

const DEBUG = false;

const logDebug = function(_target: any, key: string, descriptor: PropertyDescriptor): any {
    console.log("log(): called");

    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        const functionName = key;
        console.log(functionName + "(" + args.join(", ") + ")");
        const result = originalMethod.apply(this, args);
        console.log("=> " + result);
        return result;
    };
    return descriptor;
};
const logNoop = function() {};
const log = DEBUG ? logDebug : logNoop;

class Test {
    @log
    test(a: number, b: number) {
        console.log("test(): called", a, b);
    }
}

new Test().test(1, 2);
new Test().test(3, 5);

编译后的 JS 片段显示开销确实很小:

var Test = /** @class */ (function () {
    function Test() {
    }
    Test.prototype.test = function (a, b) {
        console.log("test(): called", a, b);
    };
    __decorate([
        log
    ], Test.prototype, "test", null);
    return Test;
}());

【讨论】:

  • 我将此标记为已接受的答案。但是,如果有一种实用的方法可以消除这种最小的开销(即加载时的 __decorate 调用),请务必提供答案。
猜你喜欢
  • 2023-01-15
  • 1970-01-01
  • 2018-06-08
  • 2012-11-18
  • 1970-01-01
  • 2017-11-15
  • 2019-07-29
  • 2014-07-31
  • 2017-01-17
相关资源
最近更新 更多