【问题标题】:Is there any way to create a document fragment from a generic piece of HTML?有没有办法从通用的 HTML 片段创建文档片段?
【发布时间】:2014-05-02 11:08:42
【问题描述】:

我正在开发一个应用程序,它使用一些客户端模板来呈现数据,并且大多数 javascript 模板引擎返回一个带有标记的简单字符串,并且由开发人员将该字符串插入到 DOM 中。

我四处搜索,看到一群人建议使用空 div,将其 innerHTML 设置为新字符串,然后像这样遍历该 div 的子节点

var parsedTemplate = 'something returned by template engine';
var tempDiv = document.createElement('div'), childNode;
var documentFragment = document.createDocumentFragment;
tempDiv.innerHTML = parsedTemplate;
while ( childNode = tempDiv.firstChild ) {
    documentFragment.appendChild(childNode);
}

而 TADA,documentFragment 现在包含已解析的模板。但是,如果我的模板恰好是 tr,则在其周围添加 div 不会实现预期的行为,因为它会将 td 的内容添加到行内。

有人知道解决这个问题的好方法吗?现在,我正在检查将插入已解析模板的节点,并根据其标签名称创建一个元素。我什至不确定还有其他方法可以做到这一点。

在搜索时,我在 w3 邮件列表中发现了this discussion,但遗憾的是没有有用的解决方案。

【问题讨论】:

  • 在您的代码示例中,临时 div 永远不会附加到文档片段中
  • @jdanyow:临时 div 的子代被追加到 while 循环中...
  • 你不能将不完整的 HTML 像 <tr><td>text</td></tr> 单独放入任何 DOM 元素中。如果这是您正在处理的模板,那么您将不得不看到一种完全不同的方法。如果您知道它是一个表格行,那么您可以通过.innerHTML 将其插入到占位符表格元素中,然后提取在占位符<table> 元素中创建的行,但是您必须特别考虑必须是在<tr><td><li>等特殊类型的容器中...
  • @jfriend00: 你不能把它放在任何 DOM 元素中,但你可以把它放在一个片段中......

标签: javascript html


【解决方案1】:

您可以使用 DOMParser 作为 XHTML 来避免 HTML “自动更正” DOM 喜欢执行:

var parser = new DOMParser(),
doc = parser.parseFromString('<tr><td>something returned </td><td>by template engine</td></tr>', "text/xml"),
documentFragment = document.createDocumentFragment() ;
documentFragment.appendChild( doc.documentElement );

//fragment populated, now view as HTML to verify fragment contains what's expected:
var temp=document.createElement('div');
temp.appendChild(documentFragment);
console.log(temp.outerHTML); 
    // shows: "<div><tr><td>something returned </td><td>by template engine</td></tr></div>"

这与使用带有临时 div 的天真 innerHTML 形成对比:

var temp=document.createElement('div');
temp.innerHTML='<tr><td>something returned </td><td>by template engine</td></tr>';
console.log(temp.outerHTML); 
// shows: '<div>something returned by template engine</div>' (bad)

通过将模板视为 XHTML/XML(确保其格式正确),我们可以改变 HTML 的正常规则。 DOMParser 的覆盖范围应该与对 documentFragment 的支持相关,但是在一些旧的 firefox 副本(个位数版本)上,您可能需要使用 importNode()。

作为一个可重用的函数:

function strToFrag(strHTML){
   var temp=document.createElement('template');
    if( temp.content ){
       temp.innerHTML=strHTML;
       return temp.content;
    }
    var parser = new DOMParser(),
    doc = parser.parseFromString(strHTML, "text/xml"),
    documentFragment = document.createDocumentFragment() ;
    documentFragment.appendChild( doc.documentElement );
    return documentFragment;
}

【讨论】:

  • @jfriend00: 可以(几乎神奇地)将 documentFragment 附加到 中。这和高性能是使用片段而不是独立 div 的主要原因......
  • 浏览器对 HTML 使用 DOMParser() 接口的支持是什么? MDN(我通常获取此类信息的来源)不是很清楚,听起来好像 Safari 和早期版本的 IE 中可能存在问题。
  • 那么,你是在告诉它你给它"text/xml",这就是为什么它可以在不支持"text/html"的浏览器中工作?有什么问题吗?
  • 这似乎在所有现代浏览器中都被破坏了。解析为 text/xml 的元素是 XML 元素,而不是 HTML 元素。它们出现在 DOM 树中,但它们在没有任何语义的情况下被渲染,就好像它们都是 span 元素一样。因此,像这样插入表格中的&lt;tr&gt; 会将其内容呈现为纯文本。
  • 现代浏览器应该使用
【解决方案2】:

这是面向未来的,而不是现在,但 HTML5 定义了一个 &lt;template&gt; 元素,它将为您创建片段。你将能够做到:

var parsedTemplate = '<tr><td>xxx</td></tr>';
var tempEL = document.createElement('template');
tempEl.innerHTML = parsedTemplate;
var documentFragment = tempEl.content;

目前可在 Firefox 中使用。 See here

【讨论】:

  • HTML5 模板非常适合这个(当支持存在时)。它在最新版本的 Chrome 和 Firefox 中受支持,但在早期版本或任何版本的 IE 中都不支持。
  • 因为,
  • template.content(如果存在模板标签支持)就像片段一样工作。请参阅我的答案中的代码和演示以获取工作版本。如果你在 Chrome 或 Firefox 中运行我的答案中的 jsFiddle,使用模板标签只需要大约 5 行代码即可解决问题。
  • 啊,我现在看到了内在的片段...... DOMParser 差不多是样板,所以我没有看到巨大的优势。我认为模板标签的重要作用不是在 Web 应用程序文档中发送模板化 HTML 时对图像和脚本执行 ping 操作,但您确实指出了一个有趣的副作用;荣誉。
【解决方案3】:

理想的方法是使用 HTML5 中的 &lt;template&gt; 标签。您可以通过编程方式创建模板元素,将.innerHTML 分配给它,所有已解析的元素(甚至是表格​​的片段)都将出现在template.content 属性中。这为您完成了所有工作。但是,目前这只存在于最新版本的 Firefox 和 Chrome 中。

如果有模板支持,就这么简单:

function makeDocFragment(htmlString) {
    var container = document.createElement("template");
    container.innerHTML = htmlString;
    return container.content;
}  

由此产生的返回结果就像documentFragment。您可以直接附加它,它可以像 documentFragment 一样解决问题,只是它具有支持 .innerHTML 分配的优势,并且它允许您使用部分形成的 HTML 片段(解决我们需要的两个问题)。

但是,模板支持还不是无处不在,因此您需要一种备用方法。处理回退的蛮力方法是查看 HTML 字符串的开头并查看它以哪种类型的选项卡开头,并为该类型的标签创建适当的容器,并使用该容器将 HTML 分配给。这是一种蛮力方法,但它有效。对于只能合法存在于特定类型容器中的任何类型的 HTML 元素,都需要这种特殊处理。我在下面的代码中包含了一堆这些类型的元素(尽管我没有试图使列表详尽无遗)。这是下面的代码和有效的 jsFiddle 链接。如果您使用最新版本的 Chrome 或 Firefox,代码将采用使用模板对象的路径。如果是其他浏览器,它会创建相应类型的容器对象。

var makeDocFragment = (function() {
    // static data in closure so it only has to be parsed once
    var specials = {
        td: {
            parentElement: "table", 
            starterHTML: "<tbody><tr class='xx_Root_'></tr></tbody>" 
        },
        tr: {
            parentElement: "table",
            starterHTML: "<tbody class='xx_Root_'></tbody>"
        },
        thead: {
            parentElement: "table",
            starterHTML: "<tbody class='xx_Root_'></tbody>"
        },
        caption: {
            parentElement: "table",
            starterHTML: "<tbody class='xx_Root_'></tbody>"
        },
        li: {
            parentElement: "ul",
        },
        dd: {
            parentElement: "dl",
        },
        dt: {
            parentElement: "dl",
        },
        optgroup: {
            parentElement: "select",
        },
        option: {
            parentElement: "select",
        }
    };

    // feature detect template tag support and use simpler path if so
    // testing for the content property is suggested by MDN
    var testTemplate = document.createElement("template");
    if ("content" in testTemplate) {
        return function(htmlString) {
            var container = document.createElement("template");
            container.innerHTML = htmlString;
            return container.content;
        }
    } else {
        return function(htmlString) {
            var specialInfo, container, root, tagMatch, 
                documentFragment;

            // can't use template tag, so lets mini-parse the first HTML tag
            // to discern if it needs a special container
            tagMatch = htmlString.match(/^\s*<([^>\s]+)/);
            if (tagMatch) {
                specialInfo = specials[tagMatch[1].toLowerCase()];
                if (specialInfo) {
                    container = document.createElement(specialInfo.parentElement);
                    if (specialInfo.starterHTML) {
                        container.innerHTML = specialInfo.starterHTML;
                    }
                    root = container.querySelector(".xx_Root_");
                    if (!root) {
                        root = container;
                    }
                    root.innerHTML = htmlString;
                }
            }
            if (!container) {
                container = document.createElement("div");
                container.innerHTML = htmlString;
                root = container;
            }
            documentFragment = document.createDocumentFragment();

            // start at the actual root we want
            while (root.firstChild) {
                documentFragment.appendChild(root.firstChild);
            }
            return documentFragment;

        }
    }
    // don't let the feature test template object hang around in closure
    testTemplate = null;
})();

// test cases
var frag = makeDocFragment("<tr><td>Three</td><td>Four</td></tr>");
document.getElementById("myTableBody").appendChild(frag);

frag = makeDocFragment("<td>Zero</td><td>Zero</td>");
document.getElementById("emptyRow").appendChild(frag);

frag = makeDocFragment("<li>Two</li><li>Three</li>");
document.getElementById("myUL").appendChild(frag);

frag = makeDocFragment("<option>Second Option</option><option>Third Option</option>");
document.getElementById("mySelect").appendChild(frag);

带有几个测试用例的工作演示:http://jsfiddle.net/jfriend00/SycL6/

【讨论】:

  • 似乎 makeDocFragment(' something returned by template engine ') 以同样的方式丢弃表格标签OP想要修复...
  • @dandavis - 您不能使用不完整的 HTML 字符串,例如 &lt;tr&gt;&lt;td&gt;something returned &lt;/td&gt;&lt;td&gt;by template engine&lt;/td&gt;&lt;/tr&gt;。这不是一个有效的 HTML 片段,它可以独立存在,存在于 div 或片段中。如果这是这里的真正问题,那么 OP 需要一种完全不同的方法。他们不能使用片段来保存不完整但已解析的 HTML。浏览器不支持。
  • 通过检查 HTML 片段中的初始标记类型并创建正确类型的容器节点来保存该类型的标记,彻底重新考虑如何执行此操作。
  • 添加了对&lt;template&gt; 标签的支持(如果可用),这是理想的解决方案,但仅在某些浏览器中支持。
  • @dandavis - template.content 就像documentFragment 一样工作。请参阅我添加到答案开头的简化示例,该示例显示了使用模板是多么简单。
【解决方案4】:

使用此功能

  • 支持 IE11
  • 必须不符合 xml,例如'测试'
    function createFragment(html){
        var tmpl = document.createElement('template');
        tmpl.innerHTML = html;
        if (tmpl.content == void 0){ // ie11
            var fragment = document.createDocumentFragment();
            var isTableEl = /^[^\S]*?<(t(?:head|body|foot|r|d|h))/i.test(html);
            tmpl.innerHTML = isTableEl ? '<table>'+html : html;
            var els        = isTableEl ? tmpl.querySelector(RegExp.$1).parentNode.childNodes : tmpl.childNodes;
            while(els[0]) fragment.appendChild(els[0]);
            return fragment;
        }
        return tmpl.content;
    }
    

    @dandavis 的解决方案将只接受 ie11 中符合 xml 的内容。
    不知道有没有其他标签需要考虑?

    【讨论】:

    猜你喜欢
    相关资源
    最近更新 更多
    热门标签