如何获取 XHTML 字符串并使用 javascript 在网页上呈现它

How to take an XHTML string and render it on a webpage using javascript

所以,我从 HTML 的可变字符串开始,它是我的用户在 RichText 编辑器中创建的 UI 模板(保存到磁盘上的 XML 文件).它将始终有效 XHTML。 XML 可以这么简单:

<div>{{FORM_PLACEHOLDER}}</div>

或者像这样复杂的东西:

<div id="user-customized-content">
    <h1 class="display-1">Customized Header</h1>
    <p class="mb-3">
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt 
        ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco 
        <strong>laboris nisi ut aliquip</strong> ex ea commodo consequat. Duis aute irure dolor in 
        reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 
        occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
    </p>
    <div class="alert alert-info">
        <p>Laboris nisi ut <em>aliquip</em> ex ea commodo consequat</p>
    </div>
    <h3 class="display-4">Lorem ipsum dolor:</h3>
    <form>{{FORM_PLACEHOLDER}}</form>
    <p class="mb-3">
        Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim 
        id est laborum.
    </p>
</div>

但它 总是 在 XML 的字符串中的某处有 {{FORM_PLACEHOLDER}}。这将指定 HTML 表单应该放置在 HTML 包装代码中的确切位置。

在我的 SPA 应用程序中(我们使用 Vue.js 但我认为使用什么 framework/library 并不重要),我通过 axios 调用检索 HTML然后我需要将 XHTML 写入我的交互式表单周围的页面(示例如下所示)。

对于 Vue.js,我们为此使用“插槽”。因此,父组件将包含表单,其子组件(下例中的 HtmlWrapper)将有一个环绕表单的插槽。

<template>
    <HtmlWrapper>
        Name: <input type="text" name="Name" v-validate="'required'" /><br />
        Email: <input type="email" name="Email" /><br />
        <button @click="dofunction()">Submit Form</button>
    </HtmlWrapper>
</template>

<script>
    import HtmlWrapper from "@/components/HtmlWrapper.vue"
</script>

我已经尝试过但不起作用的方法:

我认为需要发生的是我需要遍历 XHTML 的字符串,递归地逐个标记,并在页面上创建每个 javascript 元素,并且然后当我点击占位符时,我在那个点创建插槽(这在 Vue.js: this.$slots.default 中很容易做到)。如果不需要(一路上犯了所有最初的错误),我宁愿不重新发明轮子,所以如果有一种方法已经存在并且可用,或者有某种组件可以做到这一点,那就太好了。否则,为我指明正确的方向将是无价的。 TIA.

我不确定你是否可以提供 Vue 组件动态模板。不过,我觉得你想要的可以通过Vue3的teleport来完成。虽然感觉有点棘手,但确实有效。

我的想法是通过 v-html 呈现 XHTML,然后将您的内容传送到特定元素中,例如 #target.

如果可能的话,我建议用 <div id="target"></div> 之类的东西替换 {{FORM_PLACEHOLDER}} 以利用传送。

<template>
  <div id="example-code">
    <!-- use v-html to render the template -->
    <div v-html="template" />
    <!-- use teleport to push content to #target which will be mounted by v-html above -->
    <teleport 
      v-if="initialized" 
      to="#target"
    >
      Name: <input type="text" name="Name" v-validate="'required'" /><br />
      Email: <input type="email" name="Email" /><br />
      <button @click="dofunction()">Submit Form</button>
    </teleport>
  </div>
</template>

<script>
import { nextTick, onMounted, ref } from 'vue'

export default {
  setup() {
    const initialized = ref(false)
    // for easier explaining, I create a #target div directly in the string
    // Otherwise, it should be an empty string: ref('') here
    const template = ref('<div id="target"></div>')

    onMounted(async () => {
      // The 2 lines below are just my assumtion the way you get the XHTML string
      // const {data} = await axios.get('/template.html')
      // template.value = data

      // use the nextTick to make sure the template is rendered
      nextTick(() => {
        initialized.value = true
      })
    })
    return {
      initialized,
      template,
    }
  }
}
</script>

即使 {{FORM_PLACEHOLDER}} 必须出现在模板字符串中,我们也可以使用 replace 将其替换为 <div id="target"></div>:

str = "<form>{{FORM_PLACEHOLDER}}</form>"
re = /\{\{FORM_PLACEHOLDER\}\}/
str.replace(re, '<div id="target"></div>"')
// '<form><div id="target"></div>"</form>'

因为此 XHTML 字符串存储为 ref,所以 v-html 内容将相应更新。那么表单内容就可以如预期的那样teleported

希望能给大家带来更多新意~

我对组件基础知识做了更多研究,我认为可以使用动态模板制作组件。和我之前的回答完全不一样,所以我加上这个。

想法是:

  1. 按照你说的从 axios 获取 XHTML 字符串。
  2. {{FORM_PLACEHOLDER}} 替换为 <slot />
  3. 使用 XHTML 字符串全局注册一个名为 HtmlWrapper 的组件
  4. 导入并使用HtmlWrapper,将你想要的内容放入slot

示例代码如下:

// main.js

// import the module from vue.esm-bundler which supports runtime compilation
import { createApp } from 'vue/dist/vue.esm-bundler'
import axios from 'axios'
import App from './App.vue'

// Create a Vue application
document.addEventListener('DOMContentLoaded', async () => {
  const app = createApp(App)
  const {data} = await axios.get("./tempalte.xhtml")
  const xhtml = data.replace(/\{\{FORM_PLACEHOLDER\}\}/, '<slot />')
  // register the template
  app.component('html-wrapper', {
    template: xhtml
  })
  app.mount('#app')
})
// App.vue

<template>
  <div id="app">
    <HtmlWrapper>
      Name: <input type="text" name="Name" v-model="name" /><br />
      Email: <input type="email" name="Email" v-model="email" /><br />
      <button @click="dofunction">Submit Form</button>
    </HtmlWrapper>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const name = ref('')
    const email = ref('')
    const dofunction = () => {
      console.log('dofunction')
    }
    return {
      name,
      email,
      dofunction
    }
  }
}
</script>

你能简单地使用 .innerHTML 吗?

你有这个:

<div id="formContainer"></div>

然后把刚才从axios得到的文本赋给这个元素的.innerHTML:

receivedtextfromaxios = "<div><span>foo</span>foo{{FORM_PLACEHOLDER}}bar<span>bar</span></div>";

document.getElementById("formContainer").innerHTML = receivedtextfromaxios;

// then you use Vue the way you are used to:
Vue.createApp(...).mount('#formContainer);
根据 2021-11-28 的评论继续回答:

我认为我对问题的解释具有误导性。问题中的占位符 {{ }} 貌似和Vue没有关系。这是 RichC 的模板,不是 Vue 的。

继续 .innerHTML 讨论,下面是一个 HTML 示例,显示使用 .innerHTML 被新的 HTML 包围的实时 Vue 对象。这是一个简单的实现,它使用从 Vue 介绍文档中获取的计数器对象。 RichC 的标记更改为 << >> 以促进 reader 与 Vue 的 {{ }} 的区分。 << >> 之间的那些模板标记被简单地替换为之前存在的节点,并且忽略它们的名称。

处理这种工作有简单的方法也有困难的方法。简单的做法是用 HTML 代码替换 <<>> 之间的内容,然后使用 .innerHTML 将整个信封+组件应用到DOM。困难的方法是首先将信封 HTML 应用到 DOM,然后手动查找 DOM 树中的模板标记 << >>,然后创建那里有一个节点。

在示例中我们选择了硬方法,因为它允许我们将已经绑定到 Vue 的元素保持在它们所处的相同状态,否则我们会采用更简单的方法。

到运行,只需将上面的HTML保存为本地文件并浏览到它:

<html>
    <head>
        <script src="https://unpkg.com/vue@next"></script>
    </head>
    <body>
        <div id="container">
            text <span>span</span> counter {{ counter }} <span>span</span> text
        </div>
        <script>
            // example from https://v3.vuejs.org/guide/introduction.html
            const Counter = {
              data() { return { counter: 0 } },
              mounted() { setInterval(() => { this.counter++; }, 1000) }
            }
            Vue.createApp(Counter).mount('#container');

            function nodesWithPlaceholdersInTheTree(node, detectednodeslist) { // recursive function, get all nodes with a placeholder and store them in detectednodeslist
                if(node.nodeType==1) // 1 => element
                    for (let childnode of node.childNodes)
                        nodesWithPlaceholdersInTheTree(childnode, detectednodeslist);
                if(node.nodeType==3) { // 3 => textnode
                    const i = node.nodeValue.indexOf(">>");
                    if (i > 0) {
                        const j = node.nodeValue.indexOf(">>", i);
                        if (j > 0)
                            detectednodeslist.push(node);
                    }
                }
            }
            function stringplaceholders(inputstring) {
                // "foo<<FORM_PLACEHODER_1>>bar<<FORM_PACEHOLDER_2>>baz" => [ "FORM_PLACEHODER_1", "FORM_PACEHOLDER_2" ]
                return inputstring.split("<<").map(s=>s.slice(0,s.indexOf(">>"))).slice(1);
            }
            function replaceholder(node, nodestoinsert) { // only first placeholder is replaced
                const i = node.nodeValue.indexOf("<<");
                if (i < 0)
                    return;
                const j = node.nodeValue.indexOf(">>", i);
                if (j < 0)
                    return;
                const nodeidx = Array.from(node.parentElement.childNodes).indexOf(node);
                const textnodeafter = document.createTextNode(node.nodeValue.slice(j+2));
                node.nodeValue = node.nodeValue.slice(0,i);
                let k = 1;
                if (nodeidx < node.parentElement.childNodes.length) {
                    while (nodestoinsert.length > 0) {
                        node.parentElement.insertBefore(nodestoinsert[0],node.parentElement.childNodes[nodeidx+k]);
                        k++;
                    }
                    node.parentElement.insertBefore(textnodeafter,node.parentElement.childNodes[nodeidx+k]);
                } else {
                    while (nodestoinsert.length > 0)
                        node.parentElement.appendChild(nodestoinsert[0]);
                    node.parentElement.appendChild(textnodeafter);
                }
            }
            function inserthtmlaround(originalelement, newsurroudinghtml) {
                // moving old elments to a temporary place
                const tempcontainer = document.createElement("template");
                originalelement.visible = "hidden"; // lazy way to avoid repaiting while removing/appending
                while (originalelement.childNodes.length > 0)
                    tempcontainer.appendChild(originalelement.childNodes[0]);

                // changing to new html
                originalelement.innerHTML = newsurroudinghtml;

                // detecting << placeholders >>
                const nodeswithplaceholders = []
                nodesWithPlaceholdersInTheTree(originalelement, nodeswithplaceholders);

                // breaking textnode placeholders in two and inserting previous nodes between them
                for (node of nodeswithplaceholders)
                    replaceholder(node, tempcontainer.childNodes);
                originalelement.visible = "";
            }

            setTimeout(function(){
                inserthtmlaround(container, "TEXT <span>SPAN</span> TEXT << originalcounter >> TEXT <span>SPAN</span> TEXT");
            }, 4000);

        </script>
    </body>
</html>

解释:4 秒后,函数 inserthtmlaround 运行s。它需要容器对象的先前子节点的副本。然后它用信封 HTML 更新 .innerHTML,然后它通过查找所有 << >> 来应用 RichC 修改后的模板,对于每个有模板的节点,它打破文本节点一分为二,并在这个新的内部位置插入到先前复制的子节点(也许它应该只替换第一个实例而不是全部,但这只是一个 POC)。

实际工作需要做一些调整,但核心部分是文件中显示的部分,不要忘记在应用信封之前可以使用更简单的方法替换 RichC 的模板到 DOM 并避免操作 DOM 节点的麻烦,如果没有必要重新利用以前的对象。

为此我可以推荐一些目前效果很好的东西

创建 In vanilla,一个从 HTMLElement 扩展的自定义组件(例如)。 并实现您的逻辑,以便在您想要的地方绘制,例如在您的构造函数中(使用自定义标记)。

Simple example

    //create element
class SuperClassForm extends HTMLElement {
    constructor() {
        super();
       
    } 
    //coonect with dom
    connectedCallback() {
       
        
        this.innerHTML = this.renderTemplate("<form>...........</form>")
               
    }
       

    renderTemplate(data) {
           let htmlRender = this.innerHTML.replace("{{FORM_PLACEHOLDER}}", data)
    return htmlRender;
       
    }

}

//Init in js
 customElements.define("superclassform", SuperClassForm);
 
//define to html
<superclassform>{{FORM_PLACEHOLDER}}</superclassform>