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

时间:2018-01-31 06:08:57

标签: javascript

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

除非我代理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);
  }
}




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

这是Proxy的预期限制还是错误?

注意:当然还有其他解决方案。我可以删除代理,只是不用担心。我也可以将canvas包装在一些JavaScript对象中,该对象只暴露我需要的函数并代理它。如果这应该有效,我会更好奇。 This Mozilla blog post kind of indirectly suggests it's supposed to be possbile因为它实际上提到使用带有HTMLElement的代理,如果只是指出如果你调用someElement.appendChild(proxiedElement)它肯定会失败但是考虑到上面的简单代码我会期待它&# 39;实际上不可能有意义地包装任何DOM元素或其他本机对象。

以下是Proxies使用普通JS对象的证明。它们与基于类的工作(如函数在原型链上)。而且他们不会使用原生物体。



const img = document.createElement('img')
const proxy = new Proxy(img, {});
console.log(proxy.src);




也会因同样的错误而失败。他们没有使用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);
}

pre { margin: 0; }




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

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

1 个答案:

答案 0 :(得分:3)

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); }

这样的事情应该做你期望的事 一个可撤销的代理,带有for和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

修改

正如下面的评论所指出的那样,我完全忽略了对主机对象上的方法调用进行测试,结果证明它们都受到保护。这归结为主机对象中的怪异,它们可以按照自己的规则发挥作用。

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

我在这里假设失败的案例是Canvas,Image,Audio,Video,Promise(例如基于方法)等。我还没有对Proxies的这一部分的规范做出贡献,无论这是一个属性描述符,还是一个主机绑定的东西,但我会假设它是后者,如果不是两个。

那就是说,你能够通过以下更改覆盖它:

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;
  }
});

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

......这引起了自身的安全问题;有人可以现在缓存价值,并且可以永久访问drawImage,通过说 const draw = proxy.drawImage; ... 然后,他们已经有能力保存真实的渲染上下文,只是说 const ctx = proxy.canvas.getContext("2d"); ......所以我在这里假设某种程度的善意。

对于更安全的解决方案,还有其他一些修复,但是使用canvas,除非它只在内存中,所以任何能够读取DOM的人都可以使用上下文。