具有复制属性的 class 方法的元数据在更改源 class 的元数据时发生变化 - 打字稿

Metadata on methods of a class with copied properties changes on changing the metadata of the source class - Typescript

抱歉标题冗长。我有一个 class MutateMe 由装饰器 Decorator.

传递到一个名为 FilterFactory 的工厂
export const Decorator = (options?: DecoratorOptions) => <T extends Constructor>(target: T) => {
  new FilterFactory(target, options);
}

在这个工厂中,我将方法复制到 target class 并设置其元数据。

export class FilterFactory {
  constructor(protected target: any, options: DecoratorOptions) {
    // Getting the reference to the class from where I want to copy over methods with their own metadata
    const routesController = FilterController;

    // The class itself consists of a prefix that must be prepended to all its member methods' metadata.
    const prefixRoute = getControllerPrefix(routesController);

    console.log("For each key (member name)")
    Reflect.ownKeys(routesController.prototype).forEach(
      (property) => {
        // Ignore the primitive class methods
        if (!['constructor', 'toString', 'length'].includes(property.toString())) {
          // Copy the methods over to the `target`
          Object.defineProperty(
            target.prototype,
            property,
            Object.getOwnPropertyDescriptor(
              routesController.prototype,
              property
            )
          )

          // Prepends class metadata `filter` to each route method's metadata
          patchRoutes(target.prototype[property], prefixRoute)

          // NOTE: An alternative to prototype property assignment (Doesn't work either)
          // target.prototype[property] = routesController.prototype[property]
          
          console.log(Reflect.getOwnMetadata(PATH_METADATA, target.prototype[property]))
        }
      })
  }
}

patchRoutes函数是这样的:

const patchRoutes = <K, T extends string, P>(patchee: any, patches: (T | T[] | ((...args: P[]) => (T | T[]))), ...args: P[]) => {
  const existingPath = Reflect.getOwnMetadata(PATH_METADATA, patchee)
  if (patches instanceof Function) { patches = patches(...args) }
  if (!Array.isArray(patches)) patches = [patches]

  Reflect.defineMetadata(PATH_METADATA, (existingPath === "/" ? [...patches] : [...patches, existingPath]).join("/"), patchee)

  const createResetCallback = (resetValue, resetTarget) => () =>
    Reflect.defineMetadata(PATH_METADATA, resetValue, resetTarget)

  return createResetCallback(existingPath, patchee)
}

它 returns 一个 reset 回调来重置修补后的元数据。

现在,当我用这个装饰器装饰多个 class 时,我可以看到重复的修补。

例如,修补一次会给我 foo/filter/...,第二次调用会给我 bar/filter/filter/...

我想看看是不是方法复制不当的问题,所以,我尝试修补基础class,复制修补的方法并重置基础的元数据class :

const propertyResetCb = patchRoutes(routesController.prototype[property], prefixRoute)
...
// Assigning the property now to the target
...
// Calling the reset callback
propertyResetCb()

但是,这似乎重置了我制作的所有装饰器的 属性。

这让我相信它正在为复制的方法使用单一原型引用。我希望免费复制它们(如果您愿意,可以克隆),以便我可以独立设置它们的元数据。

此外,我更希望不必修改 patchRoutes 来考虑重复,因为最后,我想分别对它们各自的元数据进行更多修改。

谢谢:)

更新

@Mirco S. 的回答解决了我的问题。还必须添加一点元数据复制逻辑。

Reflect.defineMetadata(
    PATH_METADATA,
    Reflect.getOwnMetadata(PATH_METADATA, oldPropertyDescriptor.value),
    newPropertyDescriptor.value
)

这可能是因为 属性 描述符中的 属性 value 始终是同一个函数。没有适用于所有类型对象的通用深度复制函数,但对于函数,您可以尝试这样的操作:

// clone the propertyDescriptor to not temper with the original.
const newPropertyDescriptor = {...Object.getOwnPropertyDescriptor(
  routesController.prototype,
  property
)}

if(typeof newPropertyDescriptor.value === "function") {
  const routesControllerFunction = newPropertyDescriptor.value;
  // wrap the original function so that Reflect.defineMetadata gets applied to the
  // newly created function instead of to the prototype function of FilterController
  newPropertyDescriptor.value = (...args: any[]) => routesControllerFunction(...args);
}

Object.defineProperty(
  target.prototype,
  property,
  newPropertyDescriptor
)

如果您需要克隆的功能不止一个,则必须添加更多案例并正确克隆它们。不过要小心,如果你像这样复制函数,你就会对绑定进行调整,并且可能必须添加逻辑以在装饰的 类.

中维护适当的 this

编辑:关于装饰器的一点旁注。我自己喜欢装饰器,但经过漫长的几年后,它们仍处于第 2 阶段。当前在打字稿中实现装饰器的灵感来自 2014 年的遗留提案,该提案不再符合 current proposal。最新的提案是 WIP,据我所知,没有可用的转译(2021 年 3 月)。最新提案中有一些重大更改,因此请注意,您将来可能需要更新装饰器。不过,您可以使用遗留装饰器做的所有事情都应该可以用最新的提案来完成。我们也有可能得到另一个提议....