运行 javascript 来自外部 html 文件

Run javascript from external html file

所以我想创建和使用本机 Web 组件并将它们描述为带有标记的 HTML 文件,CSS 和 Javascript 捆绑在一个文件中,就像 Vue 做 .vue 的方式一样文件。这些组件将从外部 components.html 文件加载到页面上,例如,通过 fetch().

到目前为止,我可以毫无问题地加载 HTML 和 CSS,但是 Javascript 是“死的”,浏览器不会 运行 或者认识它。据我了解,Vue 需要一个构建步骤才能 'compile' .vue 文件。 .vue 没有实时加载。好吧,我想做实时加载。是不是很傻?

我看到的所有原生 Web 组件 'frameworks' 完全在 Javascript 中定义了它们的组件,但我想使用 HTML 并且没有模板文字 [=36] =] 定义。我基本上希望在实例化自定义元素时将方法和可能的数据附加到自定义元素。另外,eval() 不是一个可行的选择,对吧?

我想 Javascript 最初是死的,所以它不会污染全局范围。但是我怎样才能读取它并基本上将它注入自定义元素 class?

这是我的代码示例,除了在组件中加载 Javascript 外,它工作正常,因为 methods 对象在任何地方都不存在。

components.html

<template id="my-dog">
    <style>
        .fur {color: brown}
    </style>
    
    <img src="dog.gif" onclick="speak">
    <p class="fur">This is a dog</p>
    
    <script>
        methods = {
            speak(){alert("Woof!");}
        }
    </script>
</template>

模板创建脚本

//(skipping the part where I fetch components.html)
//(and inject them into the page)
//(and then iterate over each template until...)
templates.forEach(x=>{createTemplate(x)}) //ie. createTemplate('my-dog')

function createTemplate(elementName){
    
    /*various code*/

    let elemClass =  class extends HTMLElement {
        constructor() {
            super(); 
                
            this.attachShadow({mode: 'open'})
                .appendChild(templateContent.cloneNode(true));

        }
    }
    // THIS WORKS! But how can I do the same for speak() function
    // and other functions and variables defined in the template?
    elemClass.prototype['test'] = ()=>{console.log("This is a test")}

    customElements.define(elementName, elemClass);
}

正在加载外部 HTML/CSS

请参阅 Dev.To 博文:https://dev.to/dannyengelman/load-file-web-component-add-external-content-to-the-dom-1nd

正在加载外部 <script>

<script> 在全局范围内的 <template> 运行 中,一旦您将其内容克隆到 DOM

我还没有尝试过 Vue; Angular 直接 从模板中删除 所有 <script> 内容。

一个 Vanilla 解决方法是在 元素范围内添加一个 HTML 触发代码 的元素。

<img src onerror="[CODE]"> 是最有可能的候选人:

这就可以立即调用一个 GlobalFunction,或者 运行 this.getRootNode().host

console.log 执行自定义元素时显示范围:

<my-element id=ONE></my-element>
<my-element id=TWO></my-element>

<template id=scriptContainer>
  <script>
    console.log("script runs in Global scope!!");

    function GlobalFunction(scope, marker) {
      scope = scope.getRootNode().host || scope;
      console.log('run', marker, 'scope:', scope);
      //scope.elementMethod && scope.elementMethod(); // see JSFiddle
    }
  </script>

  <img src onerror="(()=>{
    // 'this' is the IMG scope, this.getRootNode().host is the Custom Element
    this.onerror = null;/* prevent endless loop if function generates an error */

    GlobalFunction(this,'fromIMGonerror');
  })()">

</template>

<my-element id=ONE></my-element>
<my-element id=TWO></my-element>

<script>
  console.log('START SCRIPT');
  customElements.define('my-element',
    class extends HTMLElement {
      connectedCallback() {
        console.log('connectedCallback', this.id, "clone Template");
        this.attachShadow({ mode: 'open' })
            .append(scriptContainer.content.cloneNode(true));
      }
    });
</script>

更详细的 playground,包括注入 SCRIPT,位于:https://jsfiddle.net/CustomElementsExamples/g134yp7v/

重要的是让你的范围正确

由于脚本 运行 在全局范围内,您可能会遇到变量冲突。

如果有 GLOBAL let foo="bar";

,模板脚本中的

let foo=42 将失败(在大多数浏览器中)

所以使用深奥的触发器“GlobalFunction”名称并且不要创建全局变量。

高级

  • 是的,相同的模板脚本 运行s 用于 每个 连接 <my-element>

  • <img src onerror="GlobalFunction.bind(this.getRootNode().host)"> 被忽略

  • 这将运行;但不要忘记重置错误函数,否则你将 运行 陷入无休止的 JS 循环

    <template id=scriptContainer>
    <script>
    console.log("script runs in Global scope!!");
    
    function GlobalFunction() {
      console.log(this); // proper Custom Element scope
    }
    </script>
    
    <img src onerror="GlobalFunction.bind(this.getRootNode().host)(this.error=null)">
    

````

  • <IMG> 侵入性更小 DOM 是使用 <style onload="...">

因此,我设法使用 here 中描述的方法使某些东西起作用,这是我在遵循 Danny Engelman 评论中的 link 之后发现的。关键基本上是使用 iframe,而不是 AJAX,直到我们在浏览器中导入 HTML 的那一天。通过使用 iframe 方法,<script> 标签内的代码保持活动状态。然后它被拉入您的页面并删除 iframe。只要代码仍然包含在 <template> 标记中,它就不会触及全局范围或实际执行任何操作,直到它在自定义元素中实例化。

关于如何最好地处理全局范围问题的问题仍然存在,但现在它只使用一个预先确定的全局变量或在模板中定义的变量,并且在 connectedCallback 我检查那些变量。如果它们存在,我将它们的信息传输到自定义元素上,然后擦除下一个连接元素的全局变量。可能有一些更好的方法来做到这一点。

这里的工作示例:https://codepen.io/lomacar/project/editor/ZPQJgg

现在,如果我可以在使用模板之前读取模板中的脚本,并将它们的代码存储在每个自定义元素指向的一个通用位置,而不是每次使用自定义元素时都处理它们的代码,那就太好了连接并且从未将他们的胆量泄漏到全局名称空间中。

更好的解决方案

好的,在寻找避免全局变量的过程中,我发现了一种完全更好的做事方式,我在这里发现了它:Inlining ECMAScript Modules in HTML。我意识到我需要使用模块,但最后的问题是如何从同一页面导入模块。好吧,事实证明,该问题的解决方案也使以前的解决方案变得不必要,尽管如果性能更高或类似的话,您可以将 iframe 策略与此策略结合使用。出于我的目的,它归结为在我的 createTemplate 函数中添加的几行,在 class 定义之前。

const mod
const templateScript = templateContent.querySelector('script')?.textContent
if (templateScript) {
    const blob = new Blob([templateScript], {type: 'application/javascript'})
    import(URL.createObjectURL(blob)).then( r =>
         mod = r
    )
}

所以它从模板中获取脚本元素作为字符串,将其转换为 blob,然后导入 blob,就好像它是一个外部模块一样!此时我可以将模块存储在任何我想要的地方,例如在构造函数中的自定义元素上。成功!