是否可以将Proxy与本机浏览器对象(HTMLElement,Canvas2DRenderingContext,...)一起使用?

gma*_*man 9 javascript

我想要完成的事情.我想分享一个画布(因为我正在做的事情非常沉重),所以我想我会做一个有限的资源管理器.您可以通过承诺询问资源,在本例中为a Canvas2DRenderingContext.它会将上下文包装在可撤销的代理中.完成后,您需要调用release,它将canvas返回给有限的资源管理器,以便它可以将其提供给其他人并且它会撤消代理,这样用户就不会再意外地使用该资源.

除非我代理Canvas2DRenderingContext它失败.

const ctx = document.createElement('canvas').getContext('2d');
const proxy = new Proxy(ctx, {});

// try to change the width of the canvas via the proxy
test(() => { proxy.canvas.width = 100; });  // ERROR

// try to translate the origin of via the proxy
test(() => { proxy.translate(1, 2); });     // ERROR


function test(fn) {
  try {
    fn();
  } catch (e) {
    console.log("FAILED:", e, fn);
  }
}
Run Code Online (Sandbox Code Playgroud)

上面的代码Uncaught TypeError: Illegal invocation在Chrome和TypeError: 'get canvas' called on an object that does not implement interface CanvasRenderingContext2D.Firefox中生成

这是Proxy的预期限制还是一个bug?

注意:当然还有其他解决方案.我可以删除代理,只是不用担心.我也可以将canvas包装在一些JavaScript对象中,该对象只暴露我需要的函数并代理它.如果这应该有效,我会更好奇.这篇Mozilla博客文章间接地暗示它应该是可能的,因为它实际上提到使用代理而HTMLElement只是指出如果你打电话它肯定会失败someElement.appendChild(proxiedElement)但是考虑到上面的简单代码我会发现它实际上不可能有意义包装任何DOM元素或其他本机对象.

下面是Proxies使用普通JS对象的证明.它们与基于类的工作(如函数在原型链上).它们不适用于本机对象.

const img = document.createElement('img')
const proxy = new Proxy(img, {});
console.log(proxy.src);
Run Code Online (Sandbox Code Playgroud)

也失败了同样的错误.他们不使用JavaScript对象的地方

function testNoOpProxy(obj, msg) {
  log(msg, '------');
  const proxy = new Proxy(obj, {});
  check("get property:", () => proxy.width);
  check("set property:", () => proxy.width = 456);
  check("get property:", () => proxy.width);
  check("call fn on object:", () => proxy.getContext('2d'));
}

function check(msg, fn) {
  let success = true;
  let r;
  try {
    r = fn();
  } catch (e) {
    success = false;
  }
  log('   ', success ? "pass" : "FAIL", msg, r, fn);
}


const test = {
  width: 123,
  getContext: function() {
    return "test";
  },
};

class Test {
  constructor() {
    this.width = 123;
  }
  getContext() {
    return `Test width = ${this.width}`;
  }
}

const testInst = new Test();
const canvas = document.createElement('canvas');

testNoOpProxy(test, 'plain object');
testNoOpProxy(testInst, 'class object');
testNoOpProxy(canvas, 'native object');



function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = [...args].join(' ');
  document.body.appendChild(elem);
}
Run Code Online (Sandbox Code Playgroud)
pre { margin: 0; }
Run Code Online (Sandbox Code Playgroud)

FWIW我选择的解决方案是将画布包装在一个小类中,它可以完成我正在使用它的东西.优点是它更容易测试(因为我可以通过模拟),我可以代理该对象没有问题.不过,我想知道

  1. 为什么代理不适用于本机对象?
  2. Proxy不能与本机对象一起使用的任何原因适用于JavaScript对象的情况吗?
  3. 是否可以让Proxy使用本机对象.

Nor*_*ard 5

const handlers = {
  get: (target, key) => key in target ? target[key] : undefined,
  set: (target, key, value) => {
    if (key in target) {
      target[key] = value;
    }
    return value;
  }
};

const { revoke, proxy } = Proxy.revocable(ctx, handlers);

// elsewhere
try {
  proxy.canvas.width = 500;
} catch (e) { console.log("Access has been revoked", e); }
Run Code Online (Sandbox Code Playgroud)

类似的事情应该符合您的期望。
一个可撤销的代理,带有用于上下文的 get 和 set 陷阱的处理程序。

请记住,当 的实例Proxy.revocable()被撤销时,对该代理的任何后续访问都将抛出异常,因此现在所有内容都需要使用 try/catch,以防它确实已被撤销。

只是为了好玩,以下是如何做完全相同的事情而不必担心抛出(就简单地使用访问器而言;不保证在您仍然有权访问时做错事):

const RevocableAccess = (item, revoked = false) => ({
  access: f => revoked ? undefined : f(item),
  revoke: () => { revoked = true; }
});

const { revoke, access: useContext } = RevocableAccess(ctx);

useContext(ctx => ctx.canvas.width = 500);
revoke();
useContext(ctx => ctx.canvas.width = 200); // never fires
Run Code Online (Sandbox Code Playgroud)

编辑

正如下面的评论所指出的,我完全忽略了测试主机对象上的方法调用,事实证明,这些方法调用都是受保护的。这归结为宿主对象的怪异,它们按照自己的规则发挥作用。

有了上面的代理,proxy.drawImage.apply(ctx, args)就可以正常工作了。然而,这是违反直觉的。

我假设这里失败的情况是 Canvas、Image、Audio、Video、Promise(例如基于方法)等。我还没有就代理的这一部分的规范进行协商,也没有讨论这是属性描述符的事情还是主机绑定的事情,但我将假设它是后者,如果不是两者兼而有之。

也就是说,您应该能够通过以下更改来覆盖它:

const { proxy, revoke } = Proxy.revocable(ctx, {
  get(object, key) {
    if (!(key in object)) {
      return undefined;
    }
    const value = object[key];
    return typeof value === "function"
      ? (...args) => value.apply(object, args)
      : value;
  }
});
Run Code Online (Sandbox Code Playgroud)

在这里,我仍然从原始对象中“获取”方法来调用它。碰巧的是,在值是函数的情况下,我调用 bind 返回一个维护this与原始上下文关系的函数。代理通常会处理这个常见的 JS 问题。

...这会引起其自身的安全问题;现在,有人可以缓存该值,并可以永久访问,比如说,drawImage通过说 const draw = proxy.drawImage;...然后,他们已经有能力保存真实的渲染上下文,只需说 const ctx = proxy.canvas.getContext("2d"); ...所以我假设一些诚信程度,在这里。

为了更安全的解决方案,还有其他修复,尽管使用画布,除非它仅在内存中,否则上下文最终将可供任何可以读取 DOM 的人使用。

  • 我认为关于为什么原始代码抛出的补充注释将是这个答案的+。为什么某些属性无法由无操作处理程序“{}”转发? (2认同)
  • 抱歉,我对代理这件事很陌生,请注意,我不是提问者。我只是注意到,虽然您的回答确实提供了问题的解决方案,但它并没有解释问题。我仍然看不到您的回答中的任何内容说明为什么在这些情况下使用抛出的无操作处理程序 OP。虽然我可以看到发生的事情,但我无法用清晰的语言向自己解释。 (2认同)
  • @gman 因此,在这方面的一个主要问题是,您现在正在面临重大的状态突变问题。缓存两个函数引用(想想“.bind”),然后调用第一个函数。哪个方法运行?现在,如果您的团队中没有人或使用您的代码将绑定方法作为回调传递,那么这是完全有效的。但如果这是库代码,为了保存函数上下文,你会破坏很多人。再说一次,如果我们说的是以 60 fps 的速度调用数百次,我明白了。在 10fps 下,我个人不这么认为。 (2认同)