【问题标题】:How to take an XHTML string and render it on a webpage using javascript如何获取 XHTML 字符串并使用 javascript 在网页上呈现它
【发布时间】:2021-12-31 03:32:35
【问题描述】:

所以,我从一个可变的 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 Wrapper 代码中的确切位置。

在我的 SPA 应用程序中(我们使用 Vue.js,但我认为使用什么框架/库并不重要),我通过 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 将发挥比您预期更大的作用。但是您可能会使用 DOMParser API 来加载 html blob。纯 javascript 中的粗略示例可能类似于 this
  • @David 我认为您可能是正确的,因为 Vue 可能需要发挥更大的作用,但我希望可以利用这一点。我需要使用这种方法渲染到页面:v3.vuejs.org/guide/render-function.html#the-virtual-dom-tree 类似这样的东西:render() { const slot = this.$slots.default ? this.$slots.default() : []; const doc = new DOMParser().parseFromString(this.htmlData.replace('{{FORM_PLACEHOLDER}}', slot),'text/html'); return h('content', {}, doc); }, 显然,由于 doc 和 slot 类型,这不起作用。
  • 我的 Vue 知识非常有限,但也许你可以使用异步组件来异步加载用户模板,注入一个 slot 标签然后渲染它。我做了一个快速example here
  • @David 可以使用未在 main.js 中全局注册的组件来完成吗?我无法让它工作。
  • 听起来你只想动态创建一个组件。输出 HTML 以便呈现。然后调用另一个事件,以便您可以创建元素并替换占位符。 css-tricks.com/…

标签: javascript json vue.js xhtml


【解决方案1】:

你能简单地使用.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 的标记改为&lt;&lt;&gt;&gt;,以方便读者区分Vue 的{{}}&lt;&lt;&gt;&gt; 之间的那些模板标记被简单地替换为之前存在的节点,并且它们的名称被忽略。

处理这类工作有简单和困难的方法。最简单的方法是简单地将&lt;&lt;&gt;&gt; 之间的内容替换为在使用.innerHTML 将整个信封+组件应用于DOM 之前预计会到达那里的HTML 代码。困难的方法是首先将信封 HTML 应用到 DOM,然后手动在 DOM 树中查找模板标记&lt;&lt;&gt;&gt;,然后在那里创建一个节点。

在示例中,我们选择了 hard 方法,因为它允许我们将已经绑定到 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 运行。它获取容器对象先前子节点的副本。然后它用信封HTML更新.innerHTML,然后它通过查找所有&lt;&lt;&gt;&gt;来应用RichC的修改模板,并且对于具有模板的每个节点,它将文本节点分成两部分并插入到先前复制的子节点这个新内部位置的节点(也许它应该只替换第一个实例而不是全部,但它只是一个 POC)。

对于实际工作,有必要进行一些调整,但核心部分是文件上显示的部分,不要忘记在将信封应用于信封之前,可以使用更简单的方法替换 RichC 的模板DOM 并避免在不需要重用之前的对象的情况下操作 DOM 节点的麻烦。

【讨论】:

  • 不幸的是,我现有的系统有多个占位符名称,具体取决于页面,因此我不能只在 main.js 中硬编码占位符。它必须是一个字符串,我传递给我的包装器组件或类似的东西。
  • 我已经用一个实时工作示例更新了答案,我称之为使用 .innerHTML 更新实时 Vue 对象。只需在浏览器中运行 HTML 文件,您就会看到。我不知道这是否是你需要的,因为我仍然可能对你真正想要的东西有误。
  • 抱歉——我不是说一个站点范围的包装器有多个占位符,我的意思是网站上的每个页面都有自己的自定义内容包装器,每个页面都可以有一个占位符该特定页面的“胆量”。
【解决方案2】:

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

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

简单示例

    //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>

【讨论】:

    【解决方案3】:

    我对组件基础进行了更多研究,我认为可以使用动态模板制作组件。这与我之前的答案完全不同,所以我添加了这个。

    想法是:

    1. 如你所说,从 axios 获取 XHTML 字符串。
    2. {{FORM_PLACEHOLDER}} 替换为&lt;slot /&gt;
    3. 使用 XHTML 字符串全局注册一个名为 HtmlWrapper 的组件
    4. 导入并使用HtmlWrapper,将你想要的内容放入槽中

    以下是示例代码:

    // 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>
    

    【讨论】:

    • 不幸的是,我现有的系统有多个占位符名称,具体取决于页面,因此我不能只在 main.js 中硬编码占位符。它必须是一个字符串,我传递给我的包装器组件或类似的东西。
    【解决方案4】:

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

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

    如果可能的话,我建议将{{FORM_PLACEHOLDER}} 替换为&lt;div id="target"&gt;&lt;/div&gt; 之类的东西以使用传送。

    <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 将其替换为&lt;div id="target"&gt;&lt;/div&gt;

    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

    希望能给大家带来更多新的想法~

    【讨论】:

    • 知道teleport 是否与@linusborg 和他的VuePortals 库有关吗?如果是这样,我的预感是 VuePortals 作为 vue2 方法可能会有所帮助,如果 vue3 不是一个选项。
    • @kevinluo201 我一直在努力让它工作,但由于我的内容是异步的,并且在已经调用 mount 之后,我无法让传送器正确连接。 Failed to locate Teleport target with selector "#inner-content". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.
    • 这就是为什么我在 中放置了一个v-if="initialized,并且它不需要在 onMounted 之后完全调用。获得异步字符串后,您可以调用nextTick(() =&gt; initialized.value = true)。 > 目标不能由组件本身渲染,理想情况下应该在整个 Vue 组件树之外。是的,我同意你的看法,这是使用瞬移的完美方式,它恰好符合我的预期。 :)
    • @kevinluo201 在使用观察器监视 html 包装器变量后,我能够成功地连接起来。一旦它被填充,我 nextTick 一个布尔变量并在那时传送它。谢谢!!
    • @RichC 感谢美味的 500 分,你让我开心。有一件事我最好告诉你:它仍然是一种非常棘手的传送方式,因为它不遵循原始设计,即“将模板传送到组件外部的目标”。以后如果teleport有任何相关的更新,你必须注意。(虽然我认为改变这种行为的可能性很小)
    猜你喜欢
    • 2015-11-02
    • 2021-03-11
    • 2012-06-07
    • 2015-06-25
    • 2018-01-12
    • 2017-06-10
    • 1970-01-01
    • 1970-01-01
    • 2018-04-21
    相关资源
    最近更新 更多