在发布版本中剥离 Typescript 装饰器
Strip Typescript decorator in release build
我有调试时 Typescript decorator @log
,它记录了 input/output/stats 个修饰函数。
我想在编译发布版本时完全去除这个特殊的 @log
装饰器。
从发布版本中删除 console.log 语句或在装饰器代码中有条件地执行操作很容易,但我想确保调用装饰器函数本身没有开销。
有什么方法可以用 Typescript 实现吗?
我的项目是基于 webpack 的。如果使用 Typescript 无法做到这一点,也许可以在后期使用 Babel 插件、UglifyJS 或其他替代插件来完成?
在问这个问题的时候,我完全忽略了一个很尴尬的小问题。装饰函数本身在每个方法声明中只被调用一次。如果装饰器在初始化时被评估为无操作函数,开销只会发生在初始化时,并且它将非常小,如下面的代码所示。
Class 对函数的实例化和运行时函数调用,用 @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;
}());
此编译时转换器从元素和命名导入中删除装饰器。
我会保持代码更新,因为它只是一个(有效的)测试。
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;
我有调试时 Typescript decorator @log
,它记录了 input/output/stats 个修饰函数。
我想在编译发布版本时完全去除这个特殊的 @log
装饰器。
从发布版本中删除 console.log 语句或在装饰器代码中有条件地执行操作很容易,但我想确保调用装饰器函数本身没有开销。
有什么方法可以用 Typescript 实现吗?
我的项目是基于 webpack 的。如果使用 Typescript 无法做到这一点,也许可以在后期使用 Babel 插件、UglifyJS 或其他替代插件来完成?
在问这个问题的时候,我完全忽略了一个很尴尬的小问题。装饰函数本身在每个方法声明中只被调用一次。如果装饰器在初始化时被评估为无操作函数,开销只会发生在初始化时,并且它将非常小,如下面的代码所示。
Class 对函数的实例化和运行时函数调用,用 @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;
}());
此编译时转换器从元素和命名导入中删除装饰器。
我会保持代码更新,因为它只是一个(有效的)测试。
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;