简体   繁体   English

如何在影子 dom/web 组件中执行 javascript?

[英]How to execute javascript in shadow dom/web components?

I'm trying to create a custom element with most of the javascript encapsulated/referenced in the template/html itself.我正在尝试创建一个自定义元素,其中大部分 javascript 封装/引用在模板/html 本身中。 How can I make that javascript from the template/element to be executed in the shadow dom?如何从要在影子 dom 中执行的模板/元素中制作 javascript? Below is an example to better understand the issue.下面是一个例子,可以更好地理解这个问题。 How can I make the script from template.innerHTML ( <script>alert("hello"); console.log("hello from tpl");</script> ) to execute?如何从template.innerHTML ( <script>alert("hello"); console.log("hello from tpl");</script> ) 执行脚本?

Currently I get no alert or logs into the console.目前我没有收到任何警报或登录控制台。 I'm testing this with Chrome.我正在用 Chrome 进行测试。

 class ViewMedia extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({mode: 'closed'}); var template = document.createElement( 'template' ); template.innerHTML = '<script>alert("hello"); console.log("hello from tpl")'; shadow.appendChild( document.importNode( template.content, true ) ); } } customElements.define('x-view-media', ViewMedia);
 <x-view-media />

A few points:几点:

  1. Browsers no longer allow you to add script via innerHTML浏览器不再允许您通过innerHTML添加脚本
  2. There is no sand-boxing of script within the DOM a web component like there is in an iFrame. DOM 中没有像 iFrame 中那样的 Web 组件的脚本沙盒。

You can create script blocks using var el = document.createElement('script');您可以使用var el = document.createElement('script');创建脚本块and then adding them as child elements.然后将它们添加为子元素。

 class ViewMedia extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({mode: 'closed'}); const s = document.createElement('script'); s.textContent = 'alert("hello");'; shadow.appendChild(s); } } customElements.define('x-view-media', ViewMedia);
 <x-view-media></x-view-media>

The reason this fails is because importNode does not evaluate scripts that were imported from another document, which is essentially what's happening when you use innerHTML to set the template content.失败的原因是importNode不会评估从另一个文档导入的脚本,这本质上是使用innerHTML设置模板内容时发生的情况。 The string you provide is parsed into a DocumentFragment which is considered a separate document.您提供的字符串将被解析为DocumentFragment ,它被视为一个单独的文档。 If the template element was selected from the main document, the scripts would be evaluated as expected :如果模板元素是从主文档中选择的,则脚本将按预期进行评估:

 <template id="temp"> <script> console.log('templated script'); </script> </template> <div id="host"></div> <script> let temp = document.querySelector('#temp'); let host = document.querySelector('#host'); let shadow = host.attachShadow({ mode:'closed' }); shadow.append(document.importNode(temp.content, true)); </script>

One way to force your scripts to evaluate would be to import them using a contextual fragment :强制脚本评估的一种方法是使用上下文片段导入它们:

 <div id="host"></div> <script> let host = document.querySelector('#host'); let shadow = host.attachShadow({ mode:'closed' }); let content = `<script> console.log(this); <\/script>`; let fragment = document.createRange().createContextualFragment(content); shadow.append(document.importNode(fragment, true)); </script>

But, this breaks encapsulation as the scripts inside your shadowRoot will actually be evaluated in the global scope with no access to your closed shadow dom.但是,这会破坏封装,因为 shadowRoot 中的脚本实际上将在全局范围内进行评估,而无法访问封闭的 shadow dom。 The method that I came up with to deal with this issue is to loop over each script in the shadow dom and evaluate it with the shadowRoot as it's scope.我想出的解决这个问题的方法是循环遍历 shadow dom 中的每个脚本,并使用 shadowRoot 对其范围进行评估。 You can't just pass the host object itself as the scope because you'll lose access to the closed shadowRoot.您不能只将主机对象本身作为范围传递,因为您将无法访问已关闭的 shadowRoot。 Instead, you can access the ShadowRoot.host property which would be available as this.host inside the embedded scripts.相反,您可以访问ShadowRoot.host属性,该属性可在嵌入式脚本中作为this.host使用。

 class TestElement extends HTMLElement { #shadowRoot = null; constructor() { super(); this.#shadowRoot = this.attachShadow({ mode:'closed' }); this.#shadowRoot.innerHTML = this.template } get template() { return ` <style>.passed{color:green}</style> <div id="test"> TEST A </div> <slot></slot> <script> let a = this.querySelector('#test'); let b = this.host.firstElementChild; a && a.classList.add('passed'); b && (b.style.color = 'green'); <\/script> `; } get #scripts() { return this.#shadowRoot.querySelectorAll('script'); } #scopedEval = (script) => Function(script).bind(this.#shadowRoot)(); #processScripts() { this.#scripts.forEach( s => this.#scopedEval(s.innerHTML) ); } connectedCallback() { this.#processScripts(); } } customElements.define('test-element', TestElement);
 <test-element> <p> TEST B </p> </test-element>

Do not use this technique with an open shadowRoot as you will leave your component vulnerable to script injection attacks.不要将此技术与打开的 shadowRoot 一起使用,因为您将使您的组件容易受到脚本注入攻击。 The browser prevents arbitrary code execution for a reason: to keep you and your users safe.浏览器阻止任意代码执行的原因是:保证您和您的用户的安全。 Do not inject untrusted content into your shadow dom with this enabled, only use this to evaluate your own scripts or trusted libraries, and ideally avoid this trick if at all possible.不要在启用此功能的情况下将不受信任的内容注入到您的 shadow dom 中,仅使用它来评估您自己的脚本或受信任的库,并且尽可能避免使用此技巧。 There are almost always better ways to execute scripts that interact with your shadow dom, like scoping all your logic into your custom element definition.几乎总是有更好的方法来执行与影子 dom 交互的脚本,例如将所有逻辑范围限定到自定义元素定义中。

Side note: Element.setHTML is a much safer method for importing untrusted content which is coming soon as part of the HTML Sanitizer API .旁注: Element.setHTML是一种更安全的导入不受信任内容的方法,该方法即将作为HTML Sanitizer API的一部分出现。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM