如何覆盖(monkeypatch)文档和元素特定的“querySelector”实现以及如何再次恢复每个原始功能?

问题描述 投票:0回答:2

我有一个本地测试环境,我想在其中临时覆盖querySelector。我知道猴子补丁是不好的做法,但在这种情况下,此代码将仅在开发人员本地使用。我写了这个片段(覆盖 querySelector 以使用选择器中名为 addonID 的另一个子字符串来获取所有选择器):

  function maybeOverrideForTestStart(partialIDOfWidget, fullIDOfWidget) {
    if(!isLocal) return;
    const addonID = fullIDOfWidget.replace(partialIDOfWidget, "");
    Element.prototype.querySelectorTemp = Element.prototype.querySelector.bind(Element);
    
    Element.prototype.querySelector = function(selector) {
      const element = this.querySelectorTemp(selector);
      if (element) return element;
      if (addonID) {
        return this.querySelectorTemp(selector + addonID) || null;
      }
    };

  }
  function maybeOverrideForTestEnd() {
    if(!isLocal) return;
    Element.prototype.querySelector = Element.querySelectorTemp;
  }

我在测试开始时调用

maybeOverrideForTestStart
,在测试结束时调用
maybeOverrideForTestEnd
。但这不起作用,而且我不确定我错过了什么。我得到的是
someElement.querySelector is not a function
"Uncaught TypeError: Illegal invocation"

注意 - 我也无法理解这是否也覆盖了 document.querySelector 和 document.body.querySelector 或者只是 someElement.querySelector。

感谢帮助,谢谢。

javascript monkeypatching queryselector defineproperty propertydescriptor
2个回答
1
投票

我会将

maybeOverrideForTestStart
的命名更改为
patchQuerySelectors
,因为它的实现也发生了变化。

为了正确地重新定义/修补修改后的

querySelector
实现(需要为
Document.prototype
Element.prototype
执行此操作)并准确恢复每个默认状态,应该选择一种利用每个原型的方法
querySelector
属性描述符。该函数确实修补每个修改版本,但也返回一个恢复每个原始设置的函数,一切都通过
Object.defineProperty

// apply the monkey patch.
const restoreDefaults = patchQuerySelectors(true, 'bar_123', 'foo');

console.log(
  'after patching ... ', {
  restoreDefaults,
  elementQuery: Element.prototype.querySelector,
  documentQuery: Document.prototype.querySelector,
});

// use the patched/modified versions of `querySelector`
console.log(
  "document.querySelector('body') ...",
  document.querySelector('body'),
);
console.log(
  "document.body.querySelector('script') ...",
  document.body.querySelector('script'),
);

// restore each specific `querySelector` to its correct default.
restoreDefaults();

console.log(
  'after restoring ... ', {
  elementQuery: Element.prototype.querySelector,
  documentQuery: Document.prototype.querySelector,
});

// use the restored versions of `querySelector`
console.log(
  "document.querySelector('body') ...",
  document.querySelector('body'),
);
console.log(
  "document.body.querySelector('script') ...",
  document.body.querySelector('script'),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// get the monkey patching right.

function patchQuerySelectors(isLocal, partialIDOfWidget, fullIDOfWidget) {
  if (!isLocal) return;

  const addonID = fullIDOfWidget.replace(partialIDOfWidget, "");
  const {
    // the original/native implementation.
    value: elementQuery,
    // the original descriptor of the native implementation.
    ...elementConfig
  } = Object.getOwnPropertyDescriptor(Element.prototype, 'querySelector');

  const {
    value: documentQuery,
    ...documentConfig
  } = Object.getOwnPropertyDescriptor(Document.prototype, 'querySelector');

  // the modified element specific `querySelector` implementation.
  function modifiedElementQuery(selector) {

    // apply the correct context to the original version.
    const element = elementQuery.call(this, selector);

    if (element) {
      return element;
    }
    if (addonID) {
    // apply the correct context to the original version.
      return elementQuery.call(this, selector + addonID);
    }
  };
  // the modified document specific `querySelector` implementation.
  function modifiedDocumentQuery(selector) {
    const element = documentQuery.call(this, selector);

    if (element) {
      return element;
    }
    if (addonID) {
      return documentQuery.call(this, selector + addonID);
    }
  };

  // - redefine the properties via the default descriptors
  //   and the newly assigned modified functions.
  Object
    .defineProperty(Element.prototype, 'querySelector', {
      value: modifiedElementQuery,
      ...elementConfig
    });
  Object
    .defineProperty(Document.prototype, 'querySelector', {
      value: modifiedDocumentQuery,
      ...documentConfig
    });

  function restoreDefaults() {
    // - redefine/restore the properties via the default descriptors
    //   and the locally stored original `querySelector` implementations.
    Object
      .defineProperty(Element.prototype, 'querySelector', {
        value: elementQuery,
        ...elementConfig,
      });
    Object
      .defineProperty(Document.prototype, 'querySelector', {
        value: documentQuery,
        ...documentConfig,
      });
  }
  return restoreDefaults;
}
</script>


1
投票

我不确定您是否需要

.bind()
该函数,但您可以在常量中保留对原始函数的引用,例如

const querySelector = Element.prototype.querySelector;

function maybeOverrideForTestStart(partialIDOfWidget, fullIDOfWidget) {
  // ....

  Element.prototype.querySelector = function (selector) {
    const element = querySelector(selector);
    if (element) return element;
    if (addonID) {
      return querySelectorTemp(selector + addonID) || null;
    }
  };
}

function maybeOverrideForTestEnd() {
  // ...
  Element.prototype.querySelector = querySelector;
}
© www.soinside.com 2019 - 2024. All rights reserved.