如何使用带有异步函数的 Babel 7 插件提案装饰器来装饰异步方法?

How do I decorate an async method using Babel 7's plugin-proposal-decorators with an async function?

我创建了一个专门由装饰器驱动的 AOP 库,它支持 Before、AfterReturning、AfterThrowing、AfterFinally 和 Around 建议(la AspectJ). It's called @scispike/aspectify.

使用所有同步代码效果很好。全部 synchronous tests pass just fine.

当我试图用异步通知(即,用一个评估为异步函数的装饰器)装饰一个异步方法时出现问题。我在发布 mocha --opts mocha.opts 'src/test/unit/**/*.spec.js' | npx bunyan 时从 Babel 收到语法错误 -- (通过 npm run u -- 参见 package.json and my mocha.opts):

$ npm --version
6.4.1
$ node --version
v10.14.2
$ npm run u
npm WARN lifecycle The node binary used for scripts is /Users/matthewadams/.asdf/shims/node but npm is using /Users/matthewadams/.asdf/installs/nodejs/10.14.2/bin/node itself. Use the `--scripts-prepend-node-path` option to include the path for the node binary npm was executed with.

> @scispike/aspectify@0.1.0-pre.1 u /Users/matthewadams/dev/scispike/aspectify
> mocha --opts mocha.opts 'src/test/unit/**/*.spec.js' | npx bunyan

/Users/matthewadams/dev/scispike/aspectify/src/test/unit/async/before.spec.js:50
        go(delayMillis, value) {
        ^^

SyntaxError: Unexpected identifier
    at new Script (vm.js:79:7)
    at createScript (vm.js:251:10)
    at Object.runInThisContext (vm.js:303:10)
    at Module._compile (internal/modules/cjs/loader.js:657:28)
    at Module._compile (/Users/matthewadams/dev/scispike/aspectify/node_modules/pirates/lib/index.js:99:24)
    at Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Object.newLoader [as .js] (/Users/matthewadams/dev/scispike/aspectify/node_modules/pirates/lib/index.js:104:7)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at /Users/matthewadams/dev/scispike/aspectify/node_modules/mocha/lib/mocha.js:250:27
    at Array.forEach (<anonymous>)
    at Mocha.loadFiles (/Users/matthewadams/dev/scispike/aspectify/node_modules/mocha/lib/mocha.js:247:14)
    at Mocha.run (/Users/matthewadams/dev/scispike/aspectify/node_modules/mocha/lib/mocha.js:576:10)
    at Object.<anonymous> (/Users/matthewadams/dev/scispike/aspectify/node_modules/mocha/bin/_mocha:637:18)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
    at startup (internal/bootstrap/node.js:282:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:743:3)
^Cmake: *** [.] Interrupt: 2
npm ERR! code ELIFECYCLE
npm ERR! errno 130
npm ERR! @scispike/aspectify@0.1.0-pre.1 u: `mocha --opts mocha.opts 'src/test/unit/**/*.spec.js' | npx bunyan`
npm ERR! Exit status 130
npm ERR!
npm ERR! Failed at the @scispike/aspectify@0.1.0-pre.1 u script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/matthewadams/.npm/_logs/2019-04-01T17_17_04_757Z-debug.log

对于那些仍然和我在一起的人,包含 before.spec.js:50 的测试是:

describe('parameterless before advice', function () {
  it('should work', async function () {
    let count = 0
    const delay = 10
    const val = 1

    const ParameterlessBeforeCount = Before(async thisJoinPoint => {
      await pause(delay + 100)
      count++
    })

    class Class {
      @ParameterlessBeforeCount
      async go (delayMillis, value) { // <-- THIS IS LINE 50
        await pause(delayMillis)
        return value
      }
    }

    const c = new Class()
    const v = await c.go(delay, val)
    expect(count).to.equal(1)
    expect(v).to.equal(val)
  })

根据@hackape 的要求编辑 1: 如果你克隆 the git repo,cd 进去,发出 npm install 然后 npm run transpile,所有代码都可以在 lib/mainlib/test 中找到。要 运行 单元测试到位,运行 npm run u; 运行 转译的单元测试,运行 npm run unit。无论如何,这是转译后的代码。

转译后的文件lib/main/Advice.js

'use strict';

/**
               * Returns a decorator that applies advice of any type.
               * @param modify [{function}] Function that takes a `thisJoinPointStaticPart` that can be used to modify the decorated member.
               * @param before [{function}] Function that takes a `thisJointPoint` that runs before execution proceeds.
               * @param afterReturning [{function}] Function that takes a `thisJointPoint` that runs after execution normally completes.
               * @param afterThrowing [{function}] Function that takes a `thisJointPoint` that runs after execution completes with an error.
               * @param afterFinally [{function}] Function that takes a `thisJointPoint` that runs after execution completes via `finally`.
               * @param around [{function}] Function that takes a `thisJointPoint` that leaves it to the developer to control behavior; no other advice functions are called.
               * @return {Function}
               * @private
               */
const Advice = ({ modify, before, afterReturning, afterThrowing, afterFinally, around } = {}) => {
  return (clazz, name, originalDescriptor) => {
    const advisedDescriptor = { ...originalDescriptor };

    let value;
    let get;
    let set;
    if (originalDescriptor.value && typeof originalDescriptor.value === 'function') value = originalDescriptor.value;
    if (originalDescriptor.get && typeof originalDescriptor.get === 'function') get = originalDescriptor.get;
    if (originalDescriptor.set && typeof originalDescriptor.set === 'function') set = originalDescriptor.set;

    const thisJoinPointStaticPart = {
      clazz,
      name,
      descriptors: {
        original: originalDescriptor,
        advised: advisedDescriptor } };


    if (get || set) thisJoinPointStaticPart.accessor = true;
    if (value) thisJoinPointStaticPart.method = true;

    if (modify) {
      modify(thisJoinPointStaticPart);
    }

    const createAdvisedFn = function (originalFn) {
      return function advisedFn(...args) {
        const thisJoinPoint = {
          thiz: this,
          args,
          fullName: thisJoinPointStaticPart.name,
          ...thisJoinPointStaticPart };

        if (thisJoinPoint.accessor) {
          if (args.length === 0) {
            thisJoinPoint.get = thisJoinPoint.fullName = `get ${thisJoinPoint.name}`;
          } else {
            thisJoinPoint.set = thisJoinPoint.fullName = `set ${thisJoinPoint.name}`;
          }
        }

        const proceed = ({ thiz, args: newArgs } = {}) => originalFn.apply(thiz || this, newArgs || args);

        if (around) {
          thisJoinPoint.proceed = proceed;
          return around(thisJoinPoint);
        }

        let returnValue;
        let error;

        try {
          if (before) {
            before(thisJoinPoint);
          }

          returnValue = proceed();

          if (afterReturning) {
            afterReturning({ returnValue, thisJoinPoint });
          }

          return returnValue;
        } catch (e) {
          error = e;
          if (afterThrowing) {
            afterThrowing({ error, thisJoinPoint });
          }
          throw error;
        } finally {
          if (afterFinally) {
            afterFinally({ returnValue, error, thisJoinPoint });
          }
        }
      };
    };

    if (value) {
      advisedDescriptor.value = createAdvisedFn(value);
    } else {
      if (get) advisedDescriptor.get = createAdvisedFn(get);
      if (set) advisedDescriptor.set = createAdvisedFn(set);
    }

    return advisedDescriptor;
  };
};

const Around = (advice, modify) => Advice({ around: advice, modify });

const Before = (advice, modify) => Advice({ before: advice, modify });

const AfterReturning = (advice, modify) => Advice({ afterReturning: advice, modify });

const AfterThrowing = (advice, modify) => Advice({ afterThrowing: advice, modify });

const AfterFinally = (advice, modify) => Advice({ afterFinally: advice, modify });

module.exports = {
  Around,
  Before,
  AfterReturning,
  AfterThrowing,
  AfterFinally };
//# sourceMappingURL=<snipped>

转译后的文件lib/test/async/before.spec.js

/* global it, describe */
'use strict';function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {var desc = {};Object.keys(descriptor).forEach(function (key) {desc[key] = descriptor[key];});desc.enumerable = !!desc.enumerable;desc.configurable = !!desc.configurable;if ('value' in desc || desc.initializer) {desc.writable = true;}desc = decorators.slice().reverse().reduce(function (desc, decorator) {return decorator(target, property, desc) || desc;}, desc);if (context && desc.initializer !== void 0) {desc.value = desc.initializer ? desc.initializer.call(context) : void 0;desc.initializer = undefined;}if (desc.initializer === void 0) {Object.defineProperty(target, property, desc);desc = null;}return desc;}

const chai = require('chai');
const expect = chai.expect;
chai.use(require('dirty-chai'));

const { Before } = require('../../../main/Advice');

const pause = require('./pause');

class Class {
  async go(delayMillis, value) {
    await pause(delayMillis);
    return value;
  }}


describe('unit tests of asynchronous before advice', function () {
  describe('base Class', function () {
    it('should work', async function () {
      const c = new Class();
      const v = await c.go(10, 1);
      expect(v).to.equal(1);
    });
    it('subclass should work', async function () {
      class Subclass extends Class {
        async go(d, v) {
          return super.go(d, v);
        }}

      const c = new Subclass();
      const v = await c.go(10, 1);
      expect(v).to.equal(1);
    });
  });
  describe('parameterless before advice', function () {
    it('should work', async function () {var _class;
      let count = 0;
      const delay = 10;
      const val = 1;

      const ParameterlessBeforeCount = Before(async thisJoinPoint => {
        await pause(delay + 100);
        count++;
      });let

      Class = (_class = class Class {
        async
        go(delayMillis, value) {
          await pause(delayMillis);
          return value;
        }}, (_applyDecoratedDescriptor(_class.prototype, "go", [ParameterlessBeforeCount], Object.getOwnPropertyDescriptor(_class.prototype, "go"), _class.prototype)), _class);


      const c = new Class();
      const v = await c.go(delay, val);
      expect(count).to.equal(1);
      expect(v).to.equal(val);
    });
  });
});
//# sourceMappingURL=<snipped>

转译后的文件lib/test/async/pause.js

'use strict';

const pause = async (ms, value) => new Promise(resolve => setTimeout(() => resolve(value), ms));

module.exports = pause;
//# sourceMappingURL=<snipped>

我尝试克隆您的存储库,设置 retainLines: false,语法错误消失了。

看起来像是 babel 中的错误,retainLines 可能无法很好地使用异步函数语法。

重现我的结果的完整步骤:

git clone git@github.com:SciSpike/aspectify.git 
git checkout async
npm install

# modified package.json ln:74 to "retainLines": false

npm run u

如果您看到不同的行为,可能是我们的环境不同。尝试从一个新的 git 克隆开始。

此外,您可以尝试使用 babel REPL 来查看哪些配置可以生成正确的代码。我为您设置了一个基本示例:link