Babel - декоратор декорированных свойств класса вызывается перед созданием экземпляра класса

Извините за создание нового вопроса, я не смог найти вопрос, касающийся этого вопроса.

У меня возникли трудности с тестированием моей инъекции зависимостей с использованием мокко и экспериментальных декораторов es6+, транспилированных с помощью babel. Декоратор свойств класса вызывается раньше, чем это должно было быть.

инъекции.test.js (тест мокко, с использованием --require babel-register)

import * as DependencyInjection from '../build/decorators/DependencyInjection';

@DependencyInjection.Injectable(service => service.injected = true)
class SampleService {

    property = 'default';

    constructor(property, ...data) {
        this.property = property || this.property;
    }
}

class Dependant {

    /** @type {SampleService} */
    @DependencyInjection.Inject(SampleService)
    sampleService;
}

describe('dependency injection', () => {

    describe('is decoratored as injectable', () => {
        it('should be injectable', done => done(SampleService.injectable ? void 0 : new Error('Injectable is not set')));

        it('should contain a predicate', done => done(SampleService.predicate ? void 0 : new Error('Predicate is not set')));
    });

    describe('inject injectable', () => {
        it ('should inject the injectable provider', done => {
            const dependant = new Dependant();

            done(!!dependant.sampleService ? void 0 : new Error('Injectable provider was not injected'));
        })
    });
});

При запуске теста украшенный класс трансформируется как задумано. Однако свойство sampleService экземпляра Dependant, созданного во втором тесте, не определено.

Рассматриваемый декоратор следует вызывать/вызывать после создания экземпляра класса, но декоратор вызывается при определении класса и оформлении свойства. Ожидаемое поведение сохраняется при использовании TypeScript.

Ниже я перечислил (упрощенные) декораторы и мою конфигурацию babel.

.babelrc

{
    "presets": [
        "env",
        "stage-0",
        "es2017"
    ],
    "plugins": [
        "syntax-decorators",
        "transform-decorators-legacy",
        ["transform-runtime", { 
            "polyfill": false,
            "regenerator": true
        }]
    ]
}

экспортированный декоратор Inject (таргетинг class property):

exports.Inject = (typeFunction, ...data) => {
    return function (target, propertyName) {
        try {
            const injected = target[propertyName] = new typeFunction(data);
            if ('predicate' in typeFunction && typeof typeFunction.predicate === 'function') {
                typeFunction.predicate(injected);
            }
        }
        catch (err) {
            throw new Error(err.message || err);
        }
    };
};

экспортированный декоратор Injectable (нацелен на class):

exports.Injectable = (predicate) => {
    return function (target) {
        const provider = target;
        provider.injectable = true;
        if (predicate && typeof predicate === 'function') {
            provider.predicate = predicate;
        }
    };
};

person Shane    schedule 15.11.2017    source источник


Ответы (1)


Я до сих пор не нашел основную причину, по которой он создает новый экземпляр класса при оформлении свойства класса. Тем не менее, я нашел рабочее решение моей проблемы. В mocha, используя --require babel-register и устаревший плагин декоратора, декоратор свойств класса использует PropertyDescriptor. Изменение упрощенного декоратора Inject на следующее решило мою проблему отсутствия создания экземпляра моего декорированного свойства класса.

exports.Inject = (typeFunction, ...data) => {
    return function (target, propertyName, descriptor) {
        let value = null;
        try {
            const injected = value = target[propertyName] = new typeFunction(...data);
            if ('predicate' in typeFunction && typeof typeFunction.predicate === 'function') {
                typeFunction.predicate(injected);
            }
        }
        catch (err) {
            throw new Error(err.message || err);
        }
        if (descriptor) {
            delete descriptor.initializer;
            delete descriptor.writable;
            descriptor.value = value;
        }
    };
};

Удаление свойства writable необходимо.

Следующие тесты...

const assert = require('assert');
const chai = require('chai');

import * as DependencyInjection from '../build/decorators/DependencyInjection';

@DependencyInjection.Injectable(service => service.injected = true)
class SampleService {

    property = 'default';

    constructor(property, ...data) {
        this.property = property || this.property;
    }
}

class Dependant {

    /** @type {SampleService} */
    @DependencyInjection.Inject(SampleService)
    sampleService;
}

class Dependant2 {

    /** @type {SampleService} */
    @DependencyInjection.Inject(SampleService, 'overloaded')
    sampleService;
}

describe('dependency injection', () => {

    describe('is decoratored as injectable', () => {
        it('should be injectable', done => done(SampleService.injectable ? void 0 : new Error('Injectable is not set')));

        it('should contain a predicate', done => done(SampleService.predicate ? void 0 : new Error('Predicate is not set')));
    });

    describe('inject at decorated class property', () => {
        it('should inject the injectable provider at the decorated property', () => {
            const dependant = new Dependant();

            chai.expect(dependant.sampleService).to.be.instanceof(SampleService, 'Injectable provider was not injected');

            chai.assert.isTrue(dependant.sampleService.injected, 'The predicate of the injectable service was not set');

            chai.expect(dependant.sampleService.property).to.equal('default', 'The initial value of \'property\' was not \'default\'');
        });

        it('should inject the injectable provider with overloaded constructor arguments at the decorated property', () => {
            const dependant = new Dependant2();

            chai.expect(dependant.sampleService).to.be.instanceOf(SampleService, 'Injectable provider was not injected');

            chai.assert.isTrue(dependant.sampleService.injected, 'The predicate of the injectable service was not set');

            chai.expect(dependant.sampleService.property).to.equal('overloaded', 'The value of \'property\' was not overloaded');
        });
    });

    describe('inject at manual target and property', () => {
        it('should inject the injectable provider at the targeting value', () => {
            const inject = DependencyInjection.Inject(SampleService);

            const target = {};

            let err = null;
            try {
                inject(target, 'service');
            } catch (e) {
                err = e;
            }

            chai.assert.isNull(err, 'Expected injection to pass');

            chai.expect(target.service).to.be.instanceOf(SampleService, 'Injectable provider was not injected');

            chai.assert.isTrue(target.service.injected, 'The predicate of the injectable service was not set');

            chai.expect(target.service.property).to.equal('default', 'The initial value of \'property\' was not \'default\'');
        });

        it('should inject the injectable provider with overloaded constructor arguments at the targeting value', () => {
            const inject = DependencyInjection.Inject(SampleService, 'overloaded');

            const target = {};

            let err = null;
            try {
                inject(target, 'service');
            } catch (e) {
                err = e;
            }

            chai.assert.isNull(err, 'Expected injection to pass');

            chai.expect(target.service).to.be.instanceOf(SampleService, 'Injectable provider was not injected');

            chai.assert.isTrue(target.service.injected, 'The predicate of the injectable service was not set');

            chai.expect(target.service.property).to.equal('overloaded', 'The value of \'property\' was not overloaded');
        });

        it('should not inject anything at the targeting value', () => {
            const inject = DependencyInjection.Inject();

            const target = {};

            let err = null;
            try {
                inject(target, 'service');
            } catch (e) {
                err = e;
            }

            chai.expect(err).to.be.instanceof(Error);

            chai.assert.notExists(target.service);
        });
    });
});

...выведите следующий результат:

  dependency injection
    is decoratored as injectable
      √ should be injectable
      √ should contain a predicate
    inject at decorated class property
      √ should inject the injectable provider at the decorated property
      √ should inject the injectable provider with overloaded constructor arguments at the decorated property
    inject at manual target and property
      √ should inject the injectable provider at the targeting value
      √ should inject the injectable provider with overloaded constructor arguments at the targeting value
      √ should not inject anything at the targeting value


  7 passing (29ms)
person Shane    schedule 16.11.2017